From 72ffc866e9bd6234d334090ff5daa876cd83b465 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Tue, 10 Sep 2024 01:25:00 +0900 Subject: [PATCH] Complement #1245: keep only `package-manager-detector`. Due to the previous PR #1245 had changed too much things, I could not follow it up. Instead, I've accepted its key feature, package manager detecting. --- benchmark/package.json | 2 +- errors/package.json | 2 +- package.json | 19 +-- packages/typescript-json/package.json | 4 +- packages/typescript-json/tsconfig.json | 3 +- src/cli/index.ts | 22 --- src/cli/subcommands/generate.ts | 42 ------ src/cli/subcommands/index.ts | 4 - src/cli/subcommands/patch.ts | 47 ------ src/cli/subcommands/setup.ts | 150 ------------------- src/cli/utils/command.ts | 6 - src/cli/utils/confFiles.ts | 30 ---- src/cli/utils/fs.ts | 105 -------------- src/cli/utils/logger.ts | 3 - src/cli/utils/message.ts | 13 -- src/cli/utils/packageManager.ts | 8 -- src/executable/TypiaGenerateWizard.ts | 83 +++++++++++ src/executable/TypiaPatchWizard.ts | 42 ++++++ src/executable/TypiaSetupWizard.ts | 160 +++++++++++++++++++++ src/executable/setup/ArgumentParser.ts | 43 ++++++ src/executable/setup/CommandExecutor.ts | 8 ++ src/executable/setup/FileRetriever.ts | 22 +++ src/executable/setup/PackageManager.ts | 86 +++++++++++ src/executable/setup/PluginConfigurator.ts | 69 +++++++++ src/executable/typia.ts | 55 +++++++ test-esm/package.json | 2 +- test/package.json | 2 +- 27 files changed, 583 insertions(+), 449 deletions(-) delete mode 100644 src/cli/index.ts delete mode 100644 src/cli/subcommands/generate.ts delete mode 100644 src/cli/subcommands/index.ts delete mode 100644 src/cli/subcommands/patch.ts delete mode 100644 src/cli/subcommands/setup.ts delete mode 100644 src/cli/utils/command.ts delete mode 100644 src/cli/utils/confFiles.ts delete mode 100644 src/cli/utils/fs.ts delete mode 100644 src/cli/utils/logger.ts delete mode 100644 src/cli/utils/message.ts delete mode 100644 src/cli/utils/packageManager.ts create mode 100644 src/executable/TypiaGenerateWizard.ts create mode 100644 src/executable/TypiaPatchWizard.ts create mode 100644 src/executable/TypiaSetupWizard.ts create mode 100644 src/executable/setup/ArgumentParser.ts create mode 100644 src/executable/setup/CommandExecutor.ts create mode 100644 src/executable/setup/FileRetriever.ts create mode 100644 src/executable/setup/PackageManager.ts create mode 100644 src/executable/setup/PluginConfigurator.ts create mode 100644 src/executable/typia.ts diff --git a/benchmark/package.json b/benchmark/package.json index c9e0d17720..ac4433e060 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -72,6 +72,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.10.0-dev.20240910.tgz" + "typia": "../typia-6.10.0-dev.20240910-2.tgz" } } \ No newline at end of file diff --git a/errors/package.json b/errors/package.json index 1483e12a6a..d05d0f2f33 100644 --- a/errors/package.json +++ b/errors/package.json @@ -32,6 +32,6 @@ "typescript": "^5.3.2" }, "dependencies": { - "typia": "../typia-6.10.0-dev.20240910.tgz" + "typia": "../typia-6.10.0-dev.20240910-2.tgz" } } \ No newline at end of file diff --git a/package.json b/package.json index 0e84b1aff6..bfc672b0aa 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "typia", - "version": "6.10.0-dev.20240910", + "version": "6.10.0-dev.20240910-2", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", "module": "lib/index.mjs", "bin": { - "typia": "./bin/typia.mjs" + "typia": "./lib/executable/typia.js" }, "tsp": { "tscOptions": { @@ -18,10 +18,8 @@ "test:bun": "bun run deploy/bun.ts", "test:template": "npm run --tag test --template", "-------------------------------------------------": "", - "build": "rimraf lib && tsc && tsc -p ./tsconfig.cli.json && rollup -c", - "cli": "node ./bin/typia.mjs", + "build": "rimraf lib && tsc && rollup -c", "dev": "rimraf lib && tsc --watch", - "dev:cli": "ts-node ./src/index.ts", "eslint": "eslint ./**/*.ts", "eslint:fix": "eslint ./**/*.ts --fix", "prettier": "prettier src --write", @@ -70,13 +68,11 @@ "homepage": "https://typia.io", "dependencies": { "@samchon/openapi": "^1.0.0", - "cleye": "^1.3.2", "commander": "^10.0.0", "comment-json": "^4.2.3", - "consola": "^3.2.3", + "inquirer": "^8.2.5", "package-manager-detector": "^0.2.0", - "randexp": "^0.5.3", - "tinyglobby": "^0.2.5" + "randexp": "^0.5.3" }, "peerDependencies": { "typescript": ">=4.8.0 <5.6.0" @@ -108,8 +104,7 @@ "README.md", "package.json", "lib", - "bin", "src" ], - "private": false -} + "private": true +} \ No newline at end of file diff --git a/packages/typescript-json/package.json b/packages/typescript-json/package.json index f45f8f73b5..7ef36d9fcc 100644 --- a/packages/typescript-json/package.json +++ b/packages/typescript-json/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json", - "version": "6.10.0-dev.20240910", + "version": "6.10.0-dev.20240910-2", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -63,7 +63,7 @@ }, "homepage": "https://typia.io", "dependencies": { - "typia": "6.10.0-dev.20240910" + "typia": "6.10.0-dev.20240910-2" }, "peerDependencies": { "typescript": ">=4.8.0 <5.6.0" diff --git a/packages/typescript-json/tsconfig.json b/packages/typescript-json/tsconfig.json index 2e9fb09c29..f3914ece9a 100644 --- a/packages/typescript-json/tsconfig.json +++ b/packages/typescript-json/tsconfig.json @@ -101,5 +101,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src"] + "include": ["src"], + "exclude": ["src/cli"] } diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 761e47512d..0000000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -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(); -} diff --git a/src/cli/subcommands/generate.ts b/src/cli/subcommands/generate.ts deleted file mode 100644 index d73c6ce111..0000000000 --- a/src/cli/subcommands/generate.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 }); - }, -); diff --git a/src/cli/subcommands/index.ts b/src/cli/subcommands/index.ts deleted file mode 100644 index 90e18fb72c..0000000000 --- a/src/cli/subcommands/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { patch } from "./patch"; -export { setup } from "./setup"; -export { generate } from "./generate"; - diff --git a/src/cli/subcommands/patch.ts b/src/cli/subcommands/patch.ts deleted file mode 100644 index 6e73037861..0000000000 --- a/src/cli/subcommands/patch.ts +++ /dev/null @@ -1,47 +0,0 @@ -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 { - 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"); - } -} diff --git a/src/cli/subcommands/setup.ts b/src/cli/subcommands/setup.ts deleted file mode 100644 index e6a72edd9d..0000000000 --- a/src/cli/subcommands/setup.ts +++ /dev/null @@ -1,150 +0,0 @@ -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; -} - -/** 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(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(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[]; diff --git a/src/cli/utils/command.ts b/src/cli/utils/command.ts deleted file mode 100644 index 7abfb29ead..0000000000 --- a/src/cli/utils/command.ts +++ /dev/null @@ -1,6 +0,0 @@ -import cp from "child_process"; - -export function run(str: string): void { - console.log(`\n$ ${str}`); - cp.execSync(str, { stdio: "inherit" }); -} diff --git a/src/cli/utils/confFiles.ts b/src/cli/utils/confFiles.ts deleted file mode 100644 index 23604c8a88..0000000000 --- a/src/cli/utils/confFiles.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 { - 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, - }, - ); -} diff --git a/src/cli/utils/fs.ts b/src/cli/utils/fs.ts deleted file mode 100644 index f9b85cb9c6..0000000000 --- a/src/cli/utils/fs.ts +++ /dev/null @@ -1,105 +0,0 @@ -// @see https://github.com/ryoppippi/bumpp/blob/e93efe88bba42bd0875f12f1c10744f41b732b6e/src/fs.ts -import * as cj from "comment-json"; -import fs from "node:fs"; -import fsPromises from "node:fs/promises"; -import path from "node:path"; -import process from "node:process"; - -/** - * Find a file in the directory hierarchy - */ -export async function findUp( - name: string | string[], - { cwd }: { cwd: string | undefined } = { cwd: process.cwd() }, -): Promise { - let directory = path.resolve(cwd ?? process.cwd()); - const { root } = path.parse(directory); - const names = [name].flat(); - - while (directory && directory !== root) { - for (const name of names) { - const filePath = path.join(directory, name); - - try { - const stats = await fsPromises.stat(filePath); - if (stats.isFile()) { - return filePath; - } - } catch {} - } - - directory = path.dirname(directory); - } - return; -} - -/** - * Describes a plain-text file. - */ -export interface TextFile { - path: string; - data: string; -} - -/** - * Describes a JSON file. - */ -interface JsonFile { - path: Readonly; - data: T & cj.CommentJSONValue; -} - -/** - * Reads a JSON file and returns the parsed data. - * This functions supports JSON/JSONC/JSON with comments. - */ -export async function readJsonFile( - name: string, - cwd: string, -): Promise> { - const file = await readTextFile(name, cwd); - const data = cj.parse(file.data) as T & cj.CommentObject; - - return { ...file, data }; -} - -/** - * Writes the given data to the specified JSON/JSONC file. - */ -export async function writeJsonFile(file: JsonFile): Promise { - const newJSON = cj.stringify(file.data, null, 2); - - return writeTextFile({ ...file, data: newJSON }); -} - -/** - * Reads a text file and returns its contents. - */ -export function readTextFile(name: string, cwd: string): Promise { - return new Promise((resolve, reject) => { - const filePath = path.isAbsolute(name) ? name : path.resolve(cwd, name); - - fs.readFile(filePath, "utf8", (err, text) => { - if (err) { - reject(err); - } else { - resolve({ - path: filePath, - data: text, - }); - } - }); - }); -} - -/** - * Writes the given text to the specified file. - */ -export function writeTextFile(file: TextFile): Promise { - return new Promise((resolve, reject) => { - fs.writeFile(file.path, file.data, (err) => { - if (err) reject(err); - else resolve(); - }); - }); -} diff --git a/src/cli/utils/logger.ts b/src/cli/utils/logger.ts deleted file mode 100644 index 2c5be55c76..0000000000 --- a/src/cli/utils/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type ConsolaInstance, consola } from "consola"; - -export const logger: ConsolaInstance = consola.withTag("typia-cli"); diff --git a/src/cli/utils/message.ts b/src/cli/utils/message.ts deleted file mode 100644 index f0461fae7f..0000000000 --- a/src/cli/utils/message.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as Logger from "../utils/logger"; - -/** - * throw an error message and exit the process - */ -export function bail(message: string): never { - Logger.logger.error(message); - process.exit(1); -} - -export function wizard(): void { - Logger.logger.box("Typia Setup Wizard"); -} diff --git a/src/cli/utils/packageManager.ts b/src/cli/utils/packageManager.ts deleted file mode 100644 index 1b656a725e..0000000000 --- a/src/cli/utils/packageManager.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { detect } from "package-manager-detector"; -import { AGENTS } from "package-manager-detector/constants"; -import { resolveCommand } from 'package-manager-detector/commands' - -type Agent = typeof AGENTS[number]; - -export { detect, AGENTS, resolveCommand }; -export type { Agent }; diff --git a/src/executable/TypiaGenerateWizard.ts b/src/executable/TypiaGenerateWizard.ts new file mode 100644 index 0000000000..f3c5e55ac4 --- /dev/null +++ b/src/executable/TypiaGenerateWizard.ts @@ -0,0 +1,83 @@ +import fs from "fs"; + +import { TypiaProgrammer } from "../programmers/TypiaProgrammer"; + +import { ArgumentParser } from "./setup/ArgumentParser"; +import { PackageManager } from "./setup/PackageManager"; + +export namespace TypiaGenerateWizard { + export async function generate(): Promise { + console.log("----------------------------------------"); + console.log(" Typia Generate Wizard"); + console.log("----------------------------------------"); + + // LOAD PACKAGE.JSON INFO + const pack: PackageManager = await PackageManager.mount(); + const options: IArguments = await ArgumentParser.parse(pack)(inquiry); + await TypiaProgrammer.build(options); + } + + const inquiry: ArgumentParser.Inquiry = async ( + _pack, + command, + prompt, + action, + ) => { + // PREPARE ASSETS + command.option("--input [path]", "input directory"); + command.option("--output [directory]", "output directory"); + command.option("--project [project]", "tsconfig.json file location"); + + const questioned = { value: false }; + + const input = (name: string) => async (message: string) => { + const result = await prompt()({ + type: "input", + name, + message, + default: "", + }); + return result[name] as string; + }; + const select = + (name: string) => + (message: string) => + async (choices: Choice[]): Promise => { + questioned.value = true; + return ( + await prompt()({ + type: "list", + name: name, + message: message, + choices: choices, + }) + )[name]; + }; + const configure = async (): Promise => { + const files: string[] = await ( + await fs.promises.readdir(process.cwd()) + ).filter( + (str) => + str.substring(0, 8) === "tsconfig" && + str.substring(str.length - 5) === ".json", + ); + if (files.length === 0) + throw new URIError(`Unable to find "tsconfig.json" file.`); + else if (files.length === 1) return files[0]!; + return select("tsconfig")("TS Config File")(files); + }; + + return action(async (options) => { + options.input ??= await input("input")("input directory"); + options.output ??= await input("output")("output directory"); + options.project ??= await configure(); + return options as IArguments; + }); + }; + + export interface IArguments { + input: string; + output: string; + project: string; + } +} diff --git a/src/executable/TypiaPatchWizard.ts b/src/executable/TypiaPatchWizard.ts new file mode 100644 index 0000000000..ac31631c0a --- /dev/null +++ b/src/executable/TypiaPatchWizard.ts @@ -0,0 +1,42 @@ +import fs from "fs"; + +export namespace TypiaPatchWizard { + export const main = async (): Promise => { + console.log("----------------------------------------"); + console.log(" Typia Setup Wizard"); + console.log("----------------------------------------"); + console.log( + [ + `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 patch(); + }; + + export const patch = async (): Promise => { + const location: string = require.resolve("typescript/lib/tsc.js"); + const content: string = await fs.promises.readFile(location, "utf8"); + if (content.indexOf(FROM_WITH_COMMENT) !== -1) + await fs.promises.writeFile( + location, + content.replace(FROM_WITH_COMMENT, TO_WITH_COMMENT), + "utf8", + ); + else if (content.indexOf(FROM_ONLY) !== -1) + await fs.promises.writeFile( + location, + content.replace(FROM_ONLY, TO_ONLY), + "utf8", + ); + }; +} + +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`; diff --git a/src/executable/TypiaSetupWizard.ts b/src/executable/TypiaSetupWizard.ts new file mode 100644 index 0000000000..a1da1cb641 --- /dev/null +++ b/src/executable/TypiaSetupWizard.ts @@ -0,0 +1,160 @@ +import fs from "fs"; +import { DetectResult, detect } from "package-manager-detector"; + +import { ArgumentParser } from "./setup/ArgumentParser"; +import { CommandExecutor } from "./setup/CommandExecutor"; +import { PackageManager } from "./setup/PackageManager"; +import { PluginConfigurator } from "./setup/PluginConfigurator"; + +export namespace TypiaSetupWizard { + export interface IArguments { + manager: "npm" | "pnpm" | "yarn" | "bun"; + project: string | null; + } + + export async function setup(): Promise { + console.log("----------------------------------------"); + console.log(" Typia Setup Wizard"); + console.log("----------------------------------------"); + + // PREPARE ASSETS + const pack: PackageManager = await PackageManager.mount(); + const args: IArguments = await ArgumentParser.parse(pack)(inquiry); + + // INSTALL TYPESCRIPT COMPILERS + pack.install({ dev: true, modulo: "typescript", version: "5.5.2" }); + pack.install({ dev: true, modulo: "ts-patch", version: "latest" }); + args.project ??= (() => { + const runner: string = pack.manager === "npm" ? "npx" : pack.manager; + CommandExecutor.run(`${runner} tsc --init`); + return (args.project = "tsconfig.json"); + })(); + + // SETUP TRANSFORMER + await pack.save((data) => { + // COMPOSE PREPARE COMMAND + data.scripts ??= {}; + if ( + typeof data.scripts.prepare === "string" && + data.scripts.prepare.trim().length + ) { + if ( + data.scripts.prepare.indexOf("ts-patch install") === -1 && + data.scripts.prepare.indexOf("typia patch") === -1 + ) + data.scripts.prepare = + "ts-patch install && typia patch && " + data.scripts.prepare; + else if (data.scripts.prepare.indexOf("ts-patch install") === -1) + data.scripts.prepare = "ts-patch install && " + data.scripts.prepare; + else if (data.scripts.prepare.indexOf("typia patch") === -1) + data.scripts.prepare = data.scripts.prepare.replace( + "ts-patch install", + "ts-patch install && typia patch", + ); + } else data.scripts.prepare = "ts-patch install && typia patch"; + + // FOR OLDER VERSIONS + if (typeof data.scripts.postinstall === "string") { + data.scripts.postinstall = data.scripts.postinstall + .split("&&") + .map((str) => str.trim()) + .filter((str) => str.indexOf("ts-patch install") === -1) + .join(" && "); + if (data.scripts.postinstall.length === 0) + delete data.scripts.postinstall; + } + }); + + // CONFIGURE TYPIA + await PluginConfigurator.configure(args); + CommandExecutor.run(`${pack.manager} run prepare`); + } + + const inquiry: ArgumentParser.Inquiry = async ( + pack, + command, + prompt, + action, + ) => { + // PREPARE ASSETS + command.option("--manager [manager", "package manager"); + command.option("--project [project]", "tsconfig.json file location"); + + // INTERNAL PROCEDURES + const questioned = { value: false }; + const select = + (name: string) => + (message: string) => + async ( + choices: Choice[], + filter?: (choice: string) => Choice, + ): Promise => { + questioned.value = true; + return ( + await prompt()({ + type: "list", + name: name, + message: message, + choices: choices, + ...(filter + ? { + filter, + } + : {}), + }) + )[name]; + }; + const configure = async (): Promise => { + const fileList: string[] = await ( + await fs.promises.readdir(process.cwd()) + ) + .filter( + (str) => + str.substring(0, 8) === "tsconfig" && + str.substring(str.length - 5) === ".json", + ) + .sort((x, y) => + x === "tsconfig.json" + ? -1 + : y === "tsconfig.json" + ? 1 + : x < y + ? -1 + : 1, + ); + if (fileList.length === 0) { + if (process.cwd() !== pack.directory) + throw new URIError(`Unable to find "tsconfig.json" file.`); + return null; + } else if (fileList.length === 1) return fileList[0]!; + return select("tsconfig")("TS Config File")(fileList); + }; + + // DO CONSTRUCT + return action(async (options) => { + pack.manager = options.manager ??= + (await detectManager()) ?? + (await select("manager")("Package Manager")( + [ + "npm" as const, + "pnpm" as const, + "bun" as const, + "yarn (berry is not supported)" as "yarn", + ], + (value) => value.split(" ")[0] as "yarn", + )); + options.project ??= await configure(); + + if (questioned.value) console.log(""); + return options as IArguments; + }); + }; + + const detectManager = async (): Promise< + "npm" | "pnpm" | "yarn" | "bun" | null + > => { + const result: DetectResult | null = await detect({ cwd: process.cwd() }); + if (result?.name === "npm") return null; // NPM case is still selectable + return result?.name ?? null; + }; +} diff --git a/src/executable/setup/ArgumentParser.ts b/src/executable/setup/ArgumentParser.ts new file mode 100644 index 0000000000..f692ab7b32 --- /dev/null +++ b/src/executable/setup/ArgumentParser.ts @@ -0,0 +1,43 @@ +import commander from "commander"; +import inquirer from "inquirer"; + +import { PackageManager } from "./PackageManager"; + +export namespace ArgumentParser { + export type Inquiry = ( + pack: PackageManager, + command: commander.Command, + prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise; + + export const parse = + (pack: PackageManager) => + async ( + inquiry: ( + pack: PackageManager, + command: commander.Command, + prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise, + ): Promise => { + // TAKE OPTIONS + const action = (closure: (options: Partial) => Promise) => + new Promise((resolve, reject) => { + commander.program.action(async (options) => { + try { + resolve(await closure(options)); + } catch (exp) { + reject(exp); + } + }); + commander.program.parseAsync().catch(reject); + }); + return inquiry( + pack, + commander.program, + inquirer.createPromptModule, + action, + ); + }; +} diff --git a/src/executable/setup/CommandExecutor.ts b/src/executable/setup/CommandExecutor.ts new file mode 100644 index 0000000000..59e7bd98ae --- /dev/null +++ b/src/executable/setup/CommandExecutor.ts @@ -0,0 +1,8 @@ +import cp from "child_process"; + +export namespace CommandExecutor { + export const run = (str: string): void => { + console.log(`\n$ ${str}`); + cp.execSync(str, { stdio: "inherit" }); + }; +} diff --git a/src/executable/setup/FileRetriever.ts b/src/executable/setup/FileRetriever.ts new file mode 100644 index 0000000000..35a5ab27b7 --- /dev/null +++ b/src/executable/setup/FileRetriever.ts @@ -0,0 +1,22 @@ +import fs from "fs"; +import path from "path"; + +export namespace FileRetriever { + export const directory = + (name: string) => + (dir: string, depth: number = 0): string | null => { + const location: string = path.join(dir, name); + if (fs.existsSync(location)) return dir; + else if (depth > 2) return null; + return directory(name)(path.join(dir, ".."), depth + 1); + }; + + export const file = + (name: string) => + (directory: string, depth: number = 0): string | null => { + const location: string = path.join(directory, name); + if (fs.existsSync(location)) return location; + else if (depth > 2) return null; + return file(name)(path.join(directory, ".."), depth + 1); + }; +} diff --git a/src/executable/setup/PackageManager.ts b/src/executable/setup/PackageManager.ts new file mode 100644 index 0000000000..fd4401fc74 --- /dev/null +++ b/src/executable/setup/PackageManager.ts @@ -0,0 +1,86 @@ +import fs from "fs"; +import path from "path"; + +import { CommandExecutor } from "./CommandExecutor"; +import { FileRetriever } from "./FileRetriever"; + +const managers = ["npm", "pnpm", "yarn", "bun"] as const; +type Manager = (typeof managers)[number]; + +export class PackageManager { + public manager: Manager = "npm"; + public get file(): string { + return path.join(this.directory, "package.json"); + } + + public static async mount(): Promise { + const location: string | null = await FileRetriever.directory( + "package.json", + )(process.cwd()); + if (location === null) + throw new URIError(`Unable to find "package.json" file`); + + return new PackageManager( + location, + await this.load(path.join(location, "package.json")), + ); + } + + public async save(modifier: (data: Package.Data) => void): Promise { + const content: string = await fs.promises.readFile(this.file, "utf8"); + this.data = JSON.parse(content); + modifier(this.data); + + return fs.promises.writeFile( + this.file, + JSON.stringify(this.data, null, 2), + "utf8", + ); + } + + public install(props: { + dev: boolean; + modulo: string; + version: string; + }): boolean { + const cmd = installCmdTable[this.manager]; + const option = props.dev ? devOptionTable[this.manager] : ""; + const middle: string = `${cmd} ${option}` as const; + CommandExecutor.run( + `${this.manager} ${middle} ${props.modulo}${ + props.version ? `@${props.version}` : "" + }`, + ); + return true; + } + + private constructor( + public readonly directory: string, + public data: Package.Data, + ) {} + + private static async load(file: string): Promise { + const content: string = await fs.promises.readFile(file, "utf8"); + return JSON.parse(content); + } +} +export namespace Package { + export interface Data { + scripts?: Record; + dependencies?: Record; + devDependencies?: Record; + } +} + +const installCmdTable = { + npm: "install", + pnpm: "add", + yarn: "add", + bun: "add", +} as const satisfies Record; +const devOptionTable = { + npm: "--save-dev", + pnpm: "--save-dev", + yarn: "--dev", + bun: "--dev", +} as const satisfies Record; diff --git a/src/executable/setup/PluginConfigurator.ts b/src/executable/setup/PluginConfigurator.ts new file mode 100644 index 0000000000..a1e80af8e1 --- /dev/null +++ b/src/executable/setup/PluginConfigurator.ts @@ -0,0 +1,69 @@ +import comments from "comment-json"; +import fs from "fs"; + +import { TypiaSetupWizard } from "../TypiaSetupWizard"; + +export namespace PluginConfigurator { + export async function configure( + args: TypiaSetupWizard.IArguments, + ): Promise { + // GET COMPILER-OPTIONS + const config: comments.CommentObject = comments.parse( + await fs.promises.readFile(args.project!, "utf8"), + ) as comments.CommentObject; + const compilerOptions = config.compilerOptions as + | comments.CommentObject + | undefined; + if (compilerOptions === undefined) + throw new ReferenceError( + `${args.project} file does not have "compilerOptions" property.`, + ); + + // PREPARE PLUGINS + const plugins: comments.CommentArray = (() => { + const plugins = compilerOptions.plugins as + | comments.CommentArray + | undefined; + if (plugins === undefined) return (compilerOptions.plugins = [] as any); + else if (!Array.isArray(plugins)) + throw new TypeError( + `"plugins" property of ${args.project} must be array type.`, + ); + return plugins; + })(); + + const strict: boolean | undefined = compilerOptions.strict as + | boolean + | undefined; + const strictNullChecks: boolean | undefined = + compilerOptions.strictNullChecks as boolean | undefined; + const oldbie: comments.CommentObject | undefined = plugins.find( + (p) => + typeof p === "object" && + p !== null && + p.transform === "typia/lib/transform", + ); + if ( + strictNullChecks !== false && + (strict === true || strictNullChecks === true) && + oldbie !== undefined + ) + return; + + // DO CONFIGURE + compilerOptions.strictNullChecks = true; + if (strict === undefined && strictNullChecks === undefined) + compilerOptions.strict = true; + if (oldbie === undefined) + plugins.push( + comments.parse(` + { + "transform": "typia/lib/transform" + }`) as comments.CommentObject, + ); + await fs.promises.writeFile( + args.project!, + comments.stringify(config, null, 2), + ); + } +} diff --git a/src/executable/typia.ts b/src/executable/typia.ts new file mode 100644 index 0000000000..bcdde5793a --- /dev/null +++ b/src/executable/typia.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env node +const USAGE = `Wrong command has been detected. Use like below: + + npx typia setup \\ + --manager (npm|pnpm|yarn) \\ + --project {tsconfig.json file path} + + - npx typia setup + - npx typia setup --manager pnpm + - npx typia setup --project tsconfig.test.json + + npx typia generate + --input {directory} \\ + --output {directory} + + --npx typia generate --input src/templates --output src/functinoal +`; + +const halt = (desc: string): never => { + console.error(desc); + process.exit(-1); +}; + +const main = async (): Promise => { + try { + await import("comment-json"); + await import("inquirer"); + await import("commander"); + } catch { + halt(`typia has not been installed. Run "npm i typia" before.`); + } + + const type: string | undefined = process.argv[2]; + if (type === "setup") { + const { TypiaSetupWizard } = await import("./TypiaSetupWizard"); + await TypiaSetupWizard.setup(); + } else if (type === "patch") { + const { TypiaPatchWizard } = await import("./TypiaPatchWizard"); + await TypiaPatchWizard.main(); + } else if (type === "generate") { + try { + await import("typescript"); + } catch { + halt( + `typescript has not been installed. Run "npm i -D typescript" before.`, + ); + } + const { TypiaGenerateWizard } = await import("./TypiaGenerateWizard"); + await TypiaGenerateWizard.generate(); + } else halt(USAGE); +}; +main().catch((exp) => { + console.error(exp); + process.exit(-1); +}); diff --git a/test-esm/package.json b/test-esm/package.json index 418a3f92df..e688ccb6f9 100644 --- a/test-esm/package.json +++ b/test-esm/package.json @@ -36,6 +36,6 @@ "typescript": "^5.4.5" }, "dependencies": { - "typia": "../typia-6.10.0-dev.20240910.tgz" + "typia": "../typia-6.10.0-dev.20240910-2.tgz" } } \ No newline at end of file diff --git a/test/package.json b/test/package.json index 6a16627ff0..b9a7515113 100644 --- a/test/package.json +++ b/test/package.json @@ -52,6 +52,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.10.0-dev.20240910.tgz" + "typia": "../typia-6.10.0-dev.20240910-2.tgz" } } \ No newline at end of file