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

feat(cli): new cli #1245

Merged
merged 21 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
bin/
!/bin
lib/
node_modules/

Expand Down
5 changes: 5 additions & 0 deletions bin/typia.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import { cli } from '../lib/cli/index.js'

await cli()
18 changes: 12 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"typings": "lib/index.d.ts",
"module": "lib/index.mjs",
"bin": {
"typia": "./lib/executable/typia.js"
"typia": "./bin/typia.mjs"
},
"tsp": {
"tscOptions": {
Expand All @@ -18,8 +18,10 @@
"test:bun": "bun run deploy/bun.ts",
"test:template": "npm run --tag test --template",
"-------------------------------------------------": "",
"build": "rimraf lib && tsc && rollup -c",
"build": "rimraf lib && tsc && tsc -p ./tsconfig.cli.json && rollup -c",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

in CJS environment, generating declarations fails for the cleye library, so I split the build command

"cli": "node ./bin/typia.mjs",
"dev": "rimraf lib && tsc --watch",
"dev:cli": "ts-node ./src/index.ts",
"eslint": "eslint ./**/*.ts",
"eslint:fix": "eslint ./**/*.ts --fix",
"prettier": "prettier src --write",
Expand Down Expand Up @@ -68,10 +70,13 @@
"homepage": "https://typia.io",
"dependencies": {
"@samchon/openapi": "^1.0.0",
"cleye": "^1.3.2",
"commander": "^10.0.0",
"comment-json": "^4.2.3",
"inquirer": "^8.2.5",
"randexp": "^0.5.3"
"consola": "^3.2.3",
"package-manager-detector": "^0.2.0",
"randexp": "^0.5.3",
"tinyglobby": "^0.2.5"
},
"peerDependencies": {
"typescript": ">=4.8.0 <5.6.0"
Expand Down Expand Up @@ -103,7 +108,8 @@
"README.md",
"package.json",
"lib",
"bin",
"src"
],
"private": true
}
"private": false
}
22 changes: 22 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { cli as cleye } from 'cleye'
import * as Subcommand from './subcommands'
import { wizard } from './utils/message';

export async function cli(){
wizard();
const argv = cleye({
name: "typia",
version: "1.0.0",
description: "CLI for Typia operations",

commands: [
Subcommand.patch,
Subcommand.generate,
Subcommand.setup,
],

})

/* if no subcommand is provided, show help */
argv.showHelp();
}
42 changes: 42 additions & 0 deletions src/cli/subcommands/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { command } from 'cleye';
import { TypiaProgrammer } from "../../programmers/TypiaProgrammer";

import * as ConfFileUtils from "../utils/confFiles";
import * as Logger from "../utils/logger";
import * as MessageUtils from "../utils/message";

export const generate = command({
name: "generate",

flags: {
input: {
type: String,
description: "input directory",
},
output: {
type: String,
description: "output directory",
},
project: {
type: String,
description: "tsconfig.json file path (e.g. ./tsconfig.test.json)",
},
},

help: {
description: "Generate Typia files",
}
}, async (argv) => {
let { input, output, project } = argv.flags;

input ??= await Logger.logger.prompt("input directory", { type: "text" });
output ??= await Logger.logger.prompt("output directory", { type: "text" });
project ??= await ConfFileUtils.findTsConfig();

if (project == null) {
MessageUtils.bail("tsconfig.json not found");
}

await TypiaProgrammer.build({ input, output, project });
},
);
4 changes: 4 additions & 0 deletions src/cli/subcommands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { patch } from "./patch";
export { setup } from "./setup";
export { generate } from "./generate";

47 changes: 47 additions & 0 deletions src/cli/subcommands/patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { command } from 'cleye';
import fs from "node:fs/promises";

import * as Logger from "../utils/logger";

const FROM_WITH_COMMENT = `var defaultJSDocParsingMode = 2 /* ParseForTypeErrors */`;
const TO_WITH_COMMENT = `var defaultJSDocParsingMode = 0 /* ParseAll */`;
const FROM_ONLY = `var defaultJSDocParsingMode = 2`;
const TO_ONLY = `var defaultJSDocParsingMode = 0`;

export const patch = command({
name: "patch",

aliases: ["p"],

help: {
description: "Extra patching for TypeScript",
}
}, async () => {
Logger.logger.info(
[
`Since TypeScript v5.3 update, "tsc" no more parses JSDoc comments.`,
``,
`Therefore, "typia" revives the JSDoc parsing feature by patching "tsc".`,
``,
`This is a temporary feature of "typia", and it would be removed when "ts-patch" being updated.`,
].join("\n"),
);

await executePatch();
Logger.logger.success("Patched TypeScript");
}
);

export async function executePatch(): Promise<void> {
const location: string = require.resolve("typescript/lib/tsc.js");
const content: string = await fs.readFile(location, "utf8");
if (!content.includes(FROM_WITH_COMMENT)) {
await fs.writeFile(
location,
content.replace(FROM_WITH_COMMENT, TO_WITH_COMMENT),
"utf8",
);
} else if (!content.includes(FROM_ONLY)) {
await fs.writeFile(location, content.replace(FROM_ONLY, TO_ONLY), "utf8");
}
}
150 changes: 150 additions & 0 deletions src/cli/subcommands/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { command } from 'cleye';
import process from "node:process";
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';

import * as PackageManager from '../utils/packageManager';
import * as CommandExecutor from "../utils/command";
import * as ConfFileUtils from "../utils/confFiles";
import * as FsUtils from "../utils/fs";
import * as Logger from "../utils/logger";
import * as MessageUtils from "../utils/message";

const TSPATCH_COMMAND = `ts-patch install`;
const TYPIA_PATCH_COMMAND = `typia patch`;
const TYPIA_TRANSFORM = `typia/lib/transform`;

/** package.json type */
interface PackageJson {
scripts?: Record<string, string>;
}

/** tsconfig.json type */
interface TSConfig {
compilerOptions?: {
strictNullChecks?: boolean;
strict?: boolean;
plugins?: {
transform: string;
}[];
};
}

/** dependency type */
interface Dependency {
dev: boolean;
modulo: string;
version: string;
}

export const setup = command({
name: "setup",

flags: {
project: {
type: String,
description: "tsconfig.json file path (e.g. ./tsconfig.test.json)",
},
},

help: {
description: "Setup Typia",
}
}, async (argv) => {
const { flags } = argv;
const cwd = process.cwd();
const manager = await PackageManager.detect({ cwd });
let agent = manager?.agent;

if (agent == null) {
const selected = await Logger.logger.prompt("Select a package manager", {
initial: "npm",
options: PackageManager.AGENTS,
}) as PackageManager.Agent;
agent = selected;
}

/* yarn@berry is not supported */
if (agent === "yarn@berry") {
MessageUtils.bail("yarn@berry is not supported.");
}

/* install dependencies */
for (const dep of DEPENDENCIES) {
const addArgs= [
`${dep.modulo}@${dep.version}`,
dep.dev ? "-D" : "",
(agent==='pnpm' || agent==='pnpm@6') && existsSync(resolve(cwd, 'pnpm-workspace.yaml')) ? '-w' : ''
]
const { command, args } = PackageManager.resolveCommand(agent, 'add', addArgs)!;
CommandExecutor.run(`${command} ${args.join(" ")}`);
}

/* === prepare package.json === */
{
const path = await FsUtils.findUp("package.json", { cwd });
if (path == null) {
MessageUtils.bail("package.json not found.");
}
const json = await FsUtils.readJsonFile<PackageJson>(path, cwd);

let prepare = (
(json.data?.scripts?.prepare as string | undefined) ?? ""
).trim();

const FULL_COMMAND = `${TSPATCH_COMMAND} && ${TYPIA_PATCH_COMMAND}`;

/* if ony `ts-patch install` is found, add `typia patch` */
prepare.replace(TSPATCH_COMMAND, FULL_COMMAND);

/* if prepare script is empty, set it to `typia patch` */
if (prepare === "") {
prepare = FULL_COMMAND;
}

/* if prepare script does not contain `typia patch`, add it */
if (prepare !== FULL_COMMAND && !prepare.includes(FULL_COMMAND)) {
prepare = `${FULL_COMMAND} && ${prepare}`;
}

/* update prepare script */
json.data.scripts = { ...(json.data.scripts ?? {}), prepare };
await FsUtils.writeJsonFile(json);
}

/* === prepare tsconfig.json === */
{
const tsConfigPath = flags.project ?? (await ConfFileUtils.findTsConfig({ cwd }));
/* if tsconfig.json is not found, create it */
if (tsConfigPath == null) {
const { command, args } = PackageManager.resolveCommand(agent, 'execute', ['tsc --init'])!;
CommandExecutor.run(`${command} ${args.join(" ")}`);
}

const tsConfig = await FsUtils.readJsonFile<TSConfig>(tsConfigPath, cwd);

if (tsConfig.data.compilerOptions == null) {
tsConfig.data.compilerOptions = {};
}

tsConfig.data.compilerOptions.strictNullChecks = true;
tsConfig.data.compilerOptions.strict = true;

tsConfig.data.compilerOptions.plugins = [
{ transform: TYPIA_TRANSFORM },
...(tsConfig.data.compilerOptions.plugins ?? []),
];
await FsUtils.writeJsonFile(tsConfig);
}

/* === run prepare script === */
const { command, args } = PackageManager.resolveCommand(agent, 'run', ['prepare'])!;
CommandExecutor.run(`${command} ${args.join(" ")}`);
},
);

/** dependencies to be installed */
const DEPENDENCIES = [
{ dev: true, modulo: "typescript", version: "5.5.2" },
{ dev: true, modulo: "ts-patch", version: "latest" },
] as const satisfies Dependency[];
6 changes: 6 additions & 0 deletions src/cli/utils/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import cp from "child_process";

export function run(str: string): void {
console.log(`\n$ ${str}`);
cp.execSync(str, { stdio: "inherit" });
}
30 changes: 30 additions & 0 deletions src/cli/utils/confFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import process from "node:process";
import { glob } from "tinyglobby";

import * as Logger from "../utils/logger";
import * as MessageUtils from "../utils/message";

export async function findTsConfig(
{ cwd }: { cwd: string } = { cwd: process.cwd() },
): Promise<string> {
const tsConfigs = await glob(["tsconfig.json", "tsconfig.*.json"], { cwd });

if (tsConfigs.length === 0) {
MessageUtils.bail("tsconfig.json not found");
}

if (tsConfigs.length === 1) {
const tsconfig = tsConfigs.at(0);
if (tsconfig != null) {
return tsconfig;
}
}

return await Logger.logger.prompt(
"Multiple tsconfig.json files found. Please specify the one to use:",
{
type: "select",
options: tsConfigs,
},
);
}
Loading