From f429a38d6e2ddb849509635c59aaa821c5ff4d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Mon, 21 Aug 2023 14:48:51 -0300 Subject: [PATCH 1/5] feat(experimental transpile): add --experimental flag, to opt to use swc transpile system --- src/cli.ts | 4 ++++ src/new/cli.ts | 9 ++++++++- src/new/form.ts | 21 ++++++++++++++++----- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 8015556..284bcdb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -22,6 +22,10 @@ yargs(hideBin(process.argv)) "$0 new expressots-demo -p yarn -t opinionated -d ./", "Create silently with path", ) + .example( + "$0 new expressots-demo -p yarn -t opinionated --experimental", + "Create with experimental swc build system" + ) .example("$0 generate service user-create", "Scaffold a service") .example("$0 info", "Show CLI details") .demandCommand(1, "You need at least one command before moving on") diff --git a/src/new/cli.ts b/src/new/cli.ts index 1bf5702..6fafbf6 100644 --- a/src/new/cli.ts +++ b/src/new/cli.ts @@ -12,7 +12,7 @@ const createProject = (): CommandModule => { } return { - command: "new [package-manager] [template] [directory]", + command: "new [package-manager] [template] [directory] [experimental]", describe: "Create a new project", builder: (yargs: Argv): Argv => { yargs @@ -37,6 +37,11 @@ const createProject = (): CommandModule => { type: "string", alias: "d", }) + .option("experimental", { + describe: "Use experimental traspile with swc", + type: "boolean", + default: false, + }) .implies("package-manager", "template") .implies("template", "package-manager"); @@ -47,11 +52,13 @@ const createProject = (): CommandModule => { packageManager, template, directory, + experimental, }) => { return await projectForm(projectName, [ packageManager, template, directory, + experimental, ]); }, }; diff --git a/src/new/form.ts b/src/new/form.ts index 430766b..8af6dd2 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -97,8 +97,10 @@ const projectForm = async (projectName: string, args: any[]): Promise => { let packageManager: PackageManager | undefined; let template: keyof typeof Template | undefined; let directory: string | undefined; + let experimental: boolean; // Resolving the argument order problem + // @todo: intent to remove for/in, see PR #17 for (const arg of args) { if (args.length >= 3) { if ( @@ -110,6 +112,8 @@ const projectForm = async (projectName: string, args: any[]): Promise => { packageManager = arg as PackageManager; } else if (arg === "non-opinionated" || arg === "opinionated") { template = arg as keyof typeof Template; + } else if (arg === true) { + experimental = arg; } else { directory = arg; } @@ -121,6 +125,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { name: projectName, packageManager: packageManager, template: Template[template], + experimental: experimental, confirm: true, }; } else { @@ -149,6 +154,12 @@ const projectForm = async (projectName: string, args: any[]): Promise => { "Opinionated :: A complete ExpressoTS project with an opinionated structure and features.", ], }, + { + type: "confirm", + name: "experimental", + message: "Use experimental traspile with swc", + default: false + }, { type: "confirm", name: "confirm", @@ -202,11 +213,11 @@ const projectForm = async (projectName: string, args: any[]): Promise => { const [_, template] = answer.template.match(/(.*) ::/) as Array; - try { - const emitter = degit( - `expressots/expressots/templates/${templates[template]}`, - ); - + try { + const emitter = experimental + ? degit(`joaoneto/expressots-opinionated-experimental#${templates[template]}`) + : degit(`expressots/expressots/templates/${templates[template]}`); + await emitter.clone(answer.name); } catch (err: any) { printError( From 26dde7af1c2de2c82a14a40fc6961e1aa9fbfbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Mon, 21 Aug 2023 19:15:56 -0300 Subject: [PATCH 2/5] fix(projectForm): change templates to use oficiall repo --- src/new/form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/new/form.ts b/src/new/form.ts index 8af6dd2..eaa95d9 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -215,7 +215,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { try { const emitter = experimental - ? degit(`joaoneto/expressots-opinionated-experimental#${templates[template]}`) + ? degit(`expressots/expressots/templates/experimental/${templates[template]}`) : degit(`expressots/expressots/templates/${templates[template]}`); await emitter.clone(answer.name); From 79b2f5fa92dbbab249ffd2a86df52adee33a5260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Mon, 21 Aug 2023 19:16:19 -0300 Subject: [PATCH 3/5] fix(projectForm): change templates to use oficiall repo (#16) --- src/new/form.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/new/form.ts b/src/new/form.ts index eaa95d9..70b2690 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -214,6 +214,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { const [_, template] = answer.template.match(/(.*) ::/) as Array; try { + // @todo: new templates, see expressots/expressots PR #79 const emitter = experimental ? degit(`expressots/expressots/templates/experimental/${templates[template]}`) : degit(`expressots/expressots/templates/${templates[template]}`); From cee5213d8c85cce32736c638689ed0af44fab29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Tue, 22 Aug 2023 23:33:02 -0300 Subject: [PATCH 4/5] feat(templatesList): add templates list for stable and experimental for interactive choices - add template types and enum - add conditional template selection - fix src/new/cli.ts bad eol --- src/@types/index.ts | 1 + src/@types/template.ts | 10 +++++++++ src/cli.ts | 2 +- src/new/cli.ts | 17 +++------------- src/new/form.ts | 46 ++++++++++++++++++++++-------------------- src/templates-list.ts | 30 +++++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 src/@types/template.ts create mode 100644 src/templates-list.ts diff --git a/src/@types/index.ts b/src/@types/index.ts index 5c62e04..2267c20 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -1 +1,2 @@ export * from "./config"; +export * from "./template"; diff --git a/src/@types/template.ts b/src/@types/template.ts new file mode 100644 index 0000000..5b79a74 --- /dev/null +++ b/src/@types/template.ts @@ -0,0 +1,10 @@ +export enum TemplateEnum { + OPINIONATED = "opinionated", + NON_OPINIONATED = "non-opinionated" +} + +export interface Template { + name: TemplateEnum; + description: string; + path: string; +} \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 284bcdb..6227759 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,7 +24,7 @@ yargs(hideBin(process.argv)) ) .example( "$0 new expressots-demo -p yarn -t opinionated --experimental", - "Create with experimental swc build system" + "Create using experimental, not battle-tested, features of ExpressoTS", ) .example("$0 generate service user-create", "Scaffold a service") .example("$0 info", "Show CLI details") diff --git a/src/new/cli.ts b/src/new/cli.ts index 6fafbf6..02464de 100644 --- a/src/new/cli.ts +++ b/src/new/cli.ts @@ -38,7 +38,7 @@ const createProject = (): CommandModule => { alias: "d", }) .option("experimental", { - describe: "Use experimental traspile with swc", + describe: "Use experimental, not battle-tested, features of ExpressoTS", type: "boolean", default: false, }) @@ -47,19 +47,8 @@ const createProject = (): CommandModule => { return yargs; }, - handler: async ({ - projectName, - packageManager, - template, - directory, - experimental, - }) => { - return await projectForm(projectName, [ - packageManager, - template, - directory, - experimental, - ]); + handler: async ({ projectName, packageManager, template, directory, experimental }) => { + return await projectForm(projectName, [packageManager, template, directory, experimental]); }, }; }; diff --git a/src/new/form.ts b/src/new/form.ts index 70b2690..7770f60 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -7,6 +7,8 @@ import fs from "node:fs"; import path from "node:path"; import { centerText } from "../utils/center-text"; import { printError } from "../utils/cli-ui"; +import { TemplateEnum } from "../@types"; +import templateList from "../templates-list"; async function packageManagerInstall({ packageManager, @@ -79,11 +81,6 @@ function changePackageName({ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } -enum Template { - "non-opinionated" = "Non-Opinionated :: A simple ExpressoTS project.", - opinionated = "Opinionated :: A complete ExpressoTS project with an opinionated structure and features.", -} - const enum PackageManager { npm = "npm", yarn = "yarn", @@ -95,7 +92,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { let answer: any; const projName: string = projectName; let packageManager: PackageManager | undefined; - let template: keyof typeof Template | undefined; + let template: TemplateEnum | undefined; let directory: string | undefined; let experimental: boolean; @@ -111,7 +108,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { ) { packageManager = arg as PackageManager; } else if (arg === "non-opinionated" || arg === "opinionated") { - template = arg as keyof typeof Template; + template = arg as TemplateEnum; } else if (arg === true) { experimental = arg; } else { @@ -124,7 +121,7 @@ const projectForm = async (projectName: string, args: any[]): Promise => { answer = { name: projectName, packageManager: packageManager, - template: Template[template], + template: template, experimental: experimental, confirm: true, }; @@ -145,20 +142,25 @@ const projectForm = async (projectName: string, args: any[]): Promise => { message: "Package manager", choices: ["npm", "yarn", "pnpm", "bun"], }, + { + type: "confirm", + name: "experimental", + message: "Use experimental, not battle-tested, features of ExpressoTS", + default: false, + }, { type: "list", name: "template", message: "Select a template", - choices: [ - "Non-Opinionated :: A simple ExpressoTS project.", - "Opinionated :: A complete ExpressoTS project with an opinionated structure and features.", - ], + when: ({ experimental }) => !experimental, + choices: templateList.stable.map(({ description }) => description), }, { - type: "confirm", - name: "experimental", - message: "Use experimental traspile with swc", - default: false + type: "list", + name: "template", + message: "Select a experimental template", + when: ({ experimental }) => experimental, + choices: templateList.experimental.map(({ description }) => description), }, { type: "confirm", @@ -179,9 +181,9 @@ const projectForm = async (projectName: string, args: any[]): Promise => { } // Hashmap of templates and their directories - const templates: Record = { - "Non-Opinionated": "non_opinionated", - Opinionated: "opinionated", + const templates: Record = { + "Non-Opinionated": TemplateEnum.NON_OPINIONATED, + Opinionated: TemplateEnum.OPINIONATED, }; if (answer.confirm) { @@ -215,9 +217,9 @@ const projectForm = async (projectName: string, args: any[]): Promise => { try { // @todo: new templates, see expressots/expressots PR #79 - const emitter = experimental - ? degit(`expressots/expressots/templates/experimental/${templates[template]}`) - : degit(`expressots/expressots/templates/${templates[template]}`); + const stability = answer.experimental ? "experimental" : "stable"; + const { path } = templateList[stability].find((item) => (item.name = templates[template])); + const emitter = degit(path); await emitter.clone(answer.name); } catch (err: any) { diff --git a/src/templates-list.ts b/src/templates-list.ts new file mode 100644 index 0000000..97ff5b4 --- /dev/null +++ b/src/templates-list.ts @@ -0,0 +1,30 @@ +import { TemplateEnum, Template } from "./@types"; + +const stable: Template[] = [ + { + name: TemplateEnum.OPINIONATED, + description: "Non-Opinionated :: A simple ExpressoTS project.", + path: "expressots/expressots/templates/non_opinionated", + }, + { + name: TemplateEnum.NON_OPINIONATED, + description: "Opinionated :: A complete ExpressoTS project with an opinionated structure and features.", + path: "expressots/expressots/templates/opinionated", + }, +]; + +const experimental: Template[] = [ + { + name: TemplateEnum.OPINIONATED, + description: "Non-Opinionated :: EXPERIMENTAL BUILD - A simple ExpressoTS project.", + path: "expressots/expressots/templates/experimental/non_opinionated", + }, + { + name: TemplateEnum.NON_OPINIONATED, + description: + "Opinionated :: EXPERIMENTAL BUILD - A complete ExpressoTS project with an opinionated structure and features.", + path: "expressots/expressots/templates/experimental/opinionated", + }, +]; + +export default { stable, experimental }; From b86b5987120116f5d7854f92ff7b94c11bacdfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Neto?= Date: Sat, 26 Aug 2023 13:02:26 -0300 Subject: [PATCH 5/5] feat(command): add dev and build commands --- package.json | 11 +++++-- src/@types/command-args.ts | 7 +++++ src/@types/index.ts | 1 + src/build/cli.ts | 13 ++++++++ src/build/form.ts | 45 +++++++++++++++++++++++++++ src/build/index.ts | 1 + src/cli.ts | 4 +++ src/dev/cli.ts | 13 ++++++++ src/dev/form.ts | 38 ++++++++++++++++++++++ src/dev/index.ts | 1 + src/index.ts | 1 + src/new/form.ts | 6 ++-- src/utils/get-platform-command-bin.ts | 6 ++++ 13 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 src/@types/command-args.ts create mode 100644 src/build/cli.ts create mode 100644 src/build/form.ts create mode 100644 src/build/index.ts create mode 100644 src/dev/cli.ts create mode 100644 src/dev/form.ts create mode 100644 src/dev/index.ts create mode 100644 src/utils/get-platform-command-bin.ts diff --git a/package.json b/package.json index fe84112..c987b99 100644 --- a/package.json +++ b/package.json @@ -48,13 +48,21 @@ }, "dependencies": { "@expressots/boost-ts": "1.1.1", + "@swc/cli": "0.1.62", + "@swc/core": "1.3.80", + "@swc/register": "0.1.10", "chalk-animation": "2.0.3", "cli-progress": "3.11.2", + "concurrently": "8.2.1", "degit": "2.8.4", "glob": "10.2.6", "inquirer": "8.0.0", "mustache": "4.2.0", + "nodemon": "3.0.1", + "rimraf": "5.0.1", "ts-node": "10.9.1", + "ts-node-dev": "2.0.0", + "tsconfig-paths": "4.2.0", "yargs": "17.6.2" }, "devDependencies": { @@ -67,6 +75,7 @@ "@types/inquirer": "^9.0.3", "@types/mustache": "^4.2.2", "@types/node": "^18.11.19", + "@types/nodemon": "^1.19.2", "@types/yargs": "^17.0.22", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", @@ -77,8 +86,6 @@ "husky": "^8.0.3", "prettier": "^2.8.4", "release-it": "^16.1.5", - "rimraf": "^4.1.2", - "ts-node-dev": "^2.0.0", "typescript": "^4.9.5", "vite": "^4.4.9", "vitest": "^0.34.4" diff --git a/src/@types/command-args.ts b/src/@types/command-args.ts new file mode 100644 index 0000000..d9749e2 --- /dev/null +++ b/src/@types/command-args.ts @@ -0,0 +1,7 @@ +export interface CommandDevArgs { + experimental: boolean; +} + +export interface CommandBuildArgs { + experimental: boolean; +} diff --git a/src/@types/index.ts b/src/@types/index.ts index 2267c20..40f2aed 100644 --- a/src/@types/index.ts +++ b/src/@types/index.ts @@ -1,2 +1,3 @@ export * from "./config"; export * from "./template"; +export * from "./command-args"; diff --git a/src/build/cli.ts b/src/build/cli.ts new file mode 100644 index 0000000..e50a445 --- /dev/null +++ b/src/build/cli.ts @@ -0,0 +1,13 @@ +import { CommandModule } from "yargs"; +import { projectForm } from "./form"; +import { CommandDevArgs } from "../@types"; + +const buildProject = (): CommandModule, CommandDevArgs> => { + return { + command: "build", + describe: "Build project", + handler: projectForm, + }; +}; + +export { buildProject }; diff --git a/src/build/form.ts b/src/build/form.ts new file mode 100644 index 0000000..f36cfe4 --- /dev/null +++ b/src/build/form.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import path from "node:path"; +import { existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { CommandBuildArgs } from "../@types"; +import { printError } from "../utils/cli-ui"; +import { getPlatformCommand } from "../utils/get-platform-command-bin"; + +const PATH = `${process.env.PATH}${path.delimiter}${path.resolve(__dirname, "../../node_modules/.bin")}`; +const env = { ...process.env, PATH }; + +const projectForm = async ({ experimental }: CommandBuildArgs): Promise => { + console.time("Build succeed"); + + spawnSync("rimraf", ["dist"], { env, stdio: "inherit" }); + + if (experimental) { + if (!existsSync(".swcrc")) { + printError("Experimental features needs .swcrc file", ".swcrc"); + process.exit(1); + } + + const { result } = require("concurrently")([ + { name: "types", command: "tsc --noEmit" }, + { name: "lint", command: "eslint src/**/*.ts" }, + { name: "build", command: "swc src -d dist" }, + ], { raw: true }); + + result + .then(() => { + console.timeEnd("Build succeed"); + }) + .catch(() => { + printError("Build failed", "expressots build"); + process.exit(1); + }); + + return; + } + + spawnSync(getPlatformCommand("tsc"), ["-p", "tsconfig.build.json"], { env, stdio: "inherit" }); + console.timeEnd("Build succeed"); +}; + +export { projectForm }; diff --git a/src/build/index.ts b/src/build/index.ts new file mode 100644 index 0000000..c1d55cf --- /dev/null +++ b/src/build/index.ts @@ -0,0 +1 @@ +export * from "./cli"; diff --git a/src/cli.ts b/src/cli.ts index 6227759..51e24fa 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,8 @@ import { hideBin } from "yargs/helpers"; import { generateProject } from "./generate"; import { infoProject } from "./info"; import { createProject } from "./new"; +import { devProject } from "./dev"; +import { buildProject } from "./build"; export const CLI_VERSION = "1.3.4"; @@ -15,6 +17,8 @@ yargs(hideBin(process.argv)) .command(createProject()) .command(generateProject()) .command(infoProject()) + .command(devProject()) + .command(buildProject()) .example("$0 new expressots-demo", "Create interactively") .example("$0 new expressots-demo -d ./", "Create interactively with path") .example("$0 new expressots-demo -p yarn -t opinionated", "Create silently") diff --git a/src/dev/cli.ts b/src/dev/cli.ts new file mode 100644 index 0000000..65dbfff --- /dev/null +++ b/src/dev/cli.ts @@ -0,0 +1,13 @@ +import { CommandModule } from "yargs"; +import { projectForm } from "./form"; +import { CommandDevArgs } from "../@types"; + +const devProject = (): CommandModule, CommandDevArgs> => { + return { + command: "dev", + describe: "Run project in development mode", + handler: projectForm, + }; +}; + +export { devProject }; diff --git a/src/dev/form.ts b/src/dev/form.ts new file mode 100644 index 0000000..3097a58 --- /dev/null +++ b/src/dev/form.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import path from "node:path"; +import { existsSync } from "node:fs"; +import { spawn, spawnSync } from "node:child_process"; +import nodemon from "nodemon"; +import { CommandDevArgs } from "../@types"; +import { printError } from "../utils/cli-ui"; +import { getPlatformCommand } from "../utils/get-platform-command-bin"; + +const projectForm = async ({ experimental }: CommandDevArgs): Promise => { + if (experimental) { + if (!existsSync(".swcrc")) { + printError("Experimental features needs .swcrc file", ".swcrc"); + process.exit(1); + } + + require('@swc/register'); + + function nodemonRestart() { + spawn(getPlatformCommand("eslint"), ["src/**/*.ts"], { stdio: "inherit" }); + spawn(getPlatformCommand("tsc"), ["--noEmit"], { stdio: "inherit" }); + } + + nodemon({ + ext: "ts", + exec: `node -r ${require.resolve("@swc/register")} src/main.ts`, + }) + .on("start", nodemonRestart) + .on("restart", nodemonRestart); + + + return; + } + + spawnSync(getPlatformCommand("ts-node-dev"), ["src/main.ts"], { stdio: "inherit" }); +}; + +export { projectForm }; diff --git a/src/dev/index.ts b/src/dev/index.ts new file mode 100644 index 0000000..c1d55cf --- /dev/null +++ b/src/dev/index.ts @@ -0,0 +1 @@ +export * from "./cli"; diff --git a/src/index.ts b/src/index.ts index 1391039..5e27bfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from "./types"; export * from "./generate"; export * from "./utils"; export * from "./new"; +export * from "./dev"; diff --git a/src/new/form.ts b/src/new/form.ts index 7770f60..85958d6 100644 --- a/src/new/form.ts +++ b/src/new/form.ts @@ -8,6 +8,7 @@ import path from "node:path"; import { centerText } from "../utils/center-text"; import { printError } from "../utils/cli-ui"; import { TemplateEnum } from "../@types"; +import { getPlatformCommand } from "../utils/get-platform-command-bin"; import templateList from "../templates-list"; async function packageManagerInstall({ @@ -20,10 +21,7 @@ async function packageManagerInstall({ progressBar: SingleBar; }) { return new Promise((resolve, reject) => { - const isWindows: boolean = process.platform === "win32"; - const command: string = isWindows - ? `${packageManager}.cmd` - : packageManager; + const command = getPlatformCommand(packageManager); const installProcess = spawn(command, ["install"], { cwd: directory, diff --git a/src/utils/get-platform-command-bin.ts b/src/utils/get-platform-command-bin.ts new file mode 100644 index 0000000..86a38e9 --- /dev/null +++ b/src/utils/get-platform-command-bin.ts @@ -0,0 +1,6 @@ +function getPlatformCommand(binName: string) { + const isWindows = process.platform === "win32"; + return isWindows ? `${binName}.cmd` : binName; +} + +export { getPlatformCommand };