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 15 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,14 @@
"homepage": "https://typia.io",
"dependencies": {
"@samchon/openapi": "^0.4.9",
"cleye": "^1.3.2",
"commander": "^10.0.0",
"comment-json": "^4.2.3",
"consola": "^3.2.3",
"inquirer": "^8.2.5",
"randexp": "^0.5.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 +109,7 @@
"README.md",
"package.json",
"lib",
"bin",
"src"
],
"private": true
}
]
}
24 changes: 24 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { cli as cleye } from 'cleye'
import { patch } from './subcommands/patch';
import { generate } from './subcommands/generate';
import { setup } from './subcommands/setup';
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: [
patch,
generate,
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 { findTsConfig } from "../utils/confFiles";
import { logger } from "../utils/logger";
import { bail } 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.prompt("input directory", { type: "text" });
output ??= await logger.prompt("output directory", { type: "text" });
project ??= await findTsConfig();

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

await TypiaProgrammer.build({ input, output, project });
},
);
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 { 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.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.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");
}
}
152 changes: 152 additions & 0 deletions src/cli/subcommands/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { command } from 'cleye';
import process from "node:process";
import { existsSync } from 'node:fs';
import { resolve } from 'node:path';
import { detect } from "package-manager-detector";
import { AGENTS } from "package-manager-detector/constants";
import { resolveCommand } from 'package-manager-detector/commands'

import { run } from "../utils/command";
import { findTsConfig } from "../utils/confFiles";
import { findUp, readJsonFile, writeJsonFile } from "../utils/fs";
import { logger } from "../utils/logger";
import { bail } 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 detect({ cwd });
let agent = manager?.agent;

if (agent == null) {
const selected = await logger.prompt("Select a package manager", {
initial: "npm",
options: AGENTS,
}) as typeof AGENTS[number];
agent = selected;
}

/* yarn@berry is not supported */
if (agent === "yarn@berry") {
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 } = resolveCommand(agent, 'add', addArgs)!;
run(`${command} ${args.join(" ")}`);
}

/* === prepare package.json === */
{
const path = await findUp("package.json", { cwd });
if (path == null) {
bail("package.json not found.");
}
const json = await 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 writeJsonFile(json);
}

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

const tsConfig = await 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 writeJsonFile(tsConfig);
}

/* === run prepare script === */
const { command, args } = resolveCommand(agent, 'run', ['prepare'])!;
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 { logger } from "./logger";
import { bail } from "./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) {
bail("tsconfig.json not found");
}

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

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