diff --git a/src/cli.ts b/src/cli.ts index c36b611..38ea823 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,302 +1,11 @@ -import { bundleAll } from "./build"; import { deploy as cdkDeploy } from "./deploy"; import { argv } from "node:process"; import { startDev } from "./dev"; import { writeFile, mkdir, watch } from "node:fs/promises"; import { resolve } from "node:path"; - -export type LogFunction = (...text: string[]) => void; -export type Command = { - title: string; - props: { name: string; required: boolean; hasValue: boolean }[]; - fn: () => any; -}; - -const commands: Command[] = [ - { - title: "deploy", - props: [], - fn: deploy, - }, - { - title: "build", - props: [], - fn: build, - }, - { - title: "dev", - props: [], - fn: dev, - }, - { - title: "create", - props: [], - fn: newApp, - }, - { - title: "get-llrt", - props: [], - fn: getLlrt, - }, -]; - -const sampleConfig = `export const name = "example-app" -export const llrt = "./llrt.zip" -`; - -const sampleGitignore = `./llrt.zip -out/ -node_modules/`; - -const samplePackage = { - type: "module", - name: "example-app", - version: "0.1.0", - devDependencies: { - lackFrw: "^0.1.0", - }, - scripts: { - dev: "lack-frw dev", - build: "lack-frw build", - deploy: "lack-frw deploy", - }, -}; - -// copied from bun init - might need to change -const sampleTsConfig = { - compilerOptions: { - lib: ["ESNext"], - target: "ESNext", - module: "ESNext", - moduleDetection: "force", - jsx: "react-jsx", - allowJs: true, - - moduleResolution: "bundler", - allowImportingTsExtensions: true, - verbatimModuleSyntax: true, - noEmit: true, - - strict: true, - skipLibCheck: true, - noFallthroughCasesInSwitch: true, - }, -}; - -const debounce = 100; -let lastTrigger = -Infinity; - -async function watchApp(log: LogFunction) { - const watcher = watch(resolve("./app"), { recursive: true }); - for await (const event of watcher) { - if (lastTrigger + debounce > Date.now()) continue; - lastTrigger = Date.now(); - const start = Date.now(); - log("Rebuilding..."); - await build(); - log(`Rebuilt in ${Date.now() - start} ms`); - } -} - -async function dev() { - const log = (...text: string[]) => { - console.log( - withHeading( - text.reduce((acc, x) => `${acc}${x}`, ""), - "Dev", - "Magenta" - ) - ); - }; - - await build(); - watchApp(log); - await startDev(log); -} - -async function getLlrt() { - const log = (text: string) => { - console.log(color(" Get LLRT ", "Blue"), text); - }; - - const config = await import("file://" + resolve("./lack.config.js")); - const path = config.llrt ?? "llrt.zip"; - - log("downloading llrt"); - const repoRes = await fetch( - "https://api.github.com/repos/awslabs/llrt/releases/latest" - ); - const data: { - assets: { - browser_download_url: string; - name: string; - }[]; - } = await repoRes.json(); - - const correctVersion = data.assets.find( - (x) => x.name === "llrt-lambda-arm64.zip" - ); - if (!correctVersion) { - error( - "Couldn't download the correct LLRT version, please set up Lack manually" - ); - return; - } - - const llrtBin = await fetch(correctVersion.browser_download_url); - const blob = await llrtBin.blob(); - await writeFile(path, Buffer.from(await blob.arrayBuffer())); - log("downloaded llrt"); -} - -async function newApp() { - const log = (text: string) => { - console.log(color(" Create ", "Gray"), text); - }; - - await getLlrt(); - - log("creating new lack app"); - log("generating config, gitignore, package.json and tsconfig"); - await writeFile(".gitignore", sampleGitignore); - await writeFile("lack.config.js", sampleConfig); - await writeFile("package.json", JSON.stringify(samplePackage, undefined, 4)); - await writeFile( - "tsconfig.json", - JSON.stringify(sampleTsConfig, undefined, 4) - ); - - await mkdir("./app"); - log("done"); -} - -async function deploy() { - const log = (text: string) => { - console.log(color(" Deploy ", "Green"), text); - }; - - log("starting deploying app"); - await build(); - log("done building, starting deploy"); - cdkDeploy(); -} - -async function build() { - const log = (text: string) => { - console.log(color(" Build ", "Blue"), text); - }; - - log("bulding app from ./app"); - const routes = await bundleAll(); - routes.forEach((x) => log(`created route ${x}`)); -} - -const colors = { - Black: "\x1b[40m", - Red: "\x1b[41m", - Green: "\x1b[42m", - Yellow: "\x1b[43m", - Blue: "\x1b[44m", - Magenta: "\x1b[45m", - Cyan: "\x1b[46m", - White: "\x1b[47m", - Gray: "\x1b[100m", - Reset: "\x1b[0m", -}; - -export function color(text: string, color: keyof typeof colors) { - return `${colors[color]}${text}${colors.Reset}`; -} - -export function withHeading( - text: string, - heading: string, - color: keyof typeof colors -) { - return `${colors[color]} ${heading} ${colors.Reset} ${text}`; -} - -export function error(...text: string[]) { - console.log( - withHeading( - text.reduce((acc, x) => `${acc}${x}`, ""), - "ERROR", - "Red" - ) - ); -} - -export function warn(...text: string[]) { - console.log( - withHeading( - text.reduce((acc, x) => `${acc}${x}`, ""), - "WARN", - "Yellow" - ) - ); -} - -function cliParser(args: string[]) { - args.splice(0, 2); // todo this might not the best idea - const commandName = args[0]; - if (!commands.map((x) => x.title).includes(commandName)) { - error("avalible commands are: ", ...commands.map((x) => `"${x.title}" `)); - return; - } - - // finish parsing command - args.splice(0, 1); - const command = commands.find((x) => x.title === commandName); - if (!command) throw new Error("couldnt find command"); - - // parse props - const props: { name: string; value?: string }[] = []; - for (const prop of args) { - // confrim proper formating of prop - if (!prop.startsWith("--")) { - error("props have to start with --"); - return; - } - const propSlice = prop.slice(2).split("="); - const setProp = command.props.find((x) => x.name === propSlice[0]); - if (!setProp) { - error( - "invalid prop, avalible ones are: ", - ...command.props.map((x) => `"${x.name}" `) - ); - return; - } - - // if the props is set as it needs a value - if (setProp.hasValue) { - if (propSlice.length !== 2) { - error( - `the prop ${setProp.name} has a value, please provide it by doing --prop=value` - ); - return; - } - - props.push({ name: setProp.name, value: propSlice[1] }); - continue; - } - if (propSlice.length === 2 && !setProp.hasValue) - warn( - `you passed a value to the prop "${setProp.name}" that actaully doesnt need it` - ); - - props.push({ name: setProp.name }); - } - - for (const required of command.props - .filter((x) => x.required) - .map((x) => x.name)) { - if (!props.map((x) => x.name).includes(required)) { - error(`the prop "${required}" needs to be set`); - return; - } - } - return { name: command.title, props, fn: command.fn } as const; -} +import { build } from "./cli/build"; +import { color, error, warn, withHeading } from "./cli/console"; +import { cliParser } from "./cli/parser"; async function main() { const params = cliParser(argv); diff --git a/src/cli/build.ts b/src/cli/build.ts new file mode 100644 index 0000000..fb2a573 --- /dev/null +++ b/src/cli/build.ts @@ -0,0 +1,12 @@ +import { bundleAll } from "../build"; +import { color } from "./console"; + +export async function build() { + const log = (text: string) => { + console.log(color(" Build ", "Blue"), text); + }; + + log("bulding app from ./app"); + const routes = await bundleAll(); + routes.forEach((x) => log(`created route ${x}`)); +} diff --git a/src/cli/console.ts b/src/cli/console.ts new file mode 100644 index 0000000..b7b2025 --- /dev/null +++ b/src/cli/console.ts @@ -0,0 +1,44 @@ +const colors = { + Black: "\x1b[40m", + Red: "\x1b[41m", + Green: "\x1b[42m", + Yellow: "\x1b[43m", + Blue: "\x1b[44m", + Magenta: "\x1b[45m", + Cyan: "\x1b[46m", + White: "\x1b[47m", + Gray: "\x1b[100m", + Reset: "\x1b[0m", +}; + +export function color(text: string, color: keyof typeof colors) { + return `${colors[color]}${text}${colors.Reset}`; +} + +export function withHeading( + text: string, + heading: string, + color: keyof typeof colors +) { + return `${colors[color]} ${heading} ${colors.Reset} ${text}`; +} + +export function error(...text: string[]) { + console.log( + withHeading( + text.reduce((acc, x) => `${acc}${x}`, ""), + "ERROR", + "Red" + ) + ); +} + +export function warn(...text: string[]) { + console.log( + withHeading( + text.reduce((acc, x) => `${acc}${x}`, ""), + "WARN", + "Yellow" + ) + ); +} diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts new file mode 100644 index 0000000..047ac90 --- /dev/null +++ b/src/cli/deploy.ts @@ -0,0 +1,14 @@ +import { build } from "./build"; +import { color } from "./console"; +import { deploy as cdkDeploy } from "../deploy"; + +export async function deploy() { + const log = (text: string) => { + console.log(color(" Deploy ", "Green"), text); + }; + + log("starting deploying app"); + await build(); + log("done building, starting deploy"); + cdkDeploy(); +} diff --git a/src/cli/dev.ts b/src/cli/dev.ts new file mode 100644 index 0000000..3d6b458 --- /dev/null +++ b/src/cli/dev.ts @@ -0,0 +1,37 @@ +import { writeFile, mkdir, watch } from "node:fs/promises"; +import { resolve } from "node:path"; +import { build } from "./build"; +import { color, error, warn, withHeading } from "./console"; +import { type LogFunction } from "./parser"; +import { startDev } from "../dev"; + +const debounce = 100; +let lastTrigger = -Infinity; + +export async function watchApp(log: LogFunction) { + const watcher = watch(resolve("./app"), { recursive: true }); + for await (const event of watcher) { + if (lastTrigger + debounce > Date.now()) continue; + lastTrigger = Date.now(); + const start = Date.now(); + log("Rebuilding..."); + await build(); + log(`Rebuilt in ${Date.now() - start} ms`); + } +} + +export async function dev() { + const log = (...text: string[]) => { + console.log( + withHeading( + text.reduce((acc, x) => `${acc}${x}`, ""), + "Dev", + "Magenta" + ) + ); + }; + + await build(); + watchApp(log); + await startDev(log); +} diff --git a/src/cli/get_llrt.ts b/src/cli/get_llrt.ts new file mode 100644 index 0000000..739f643 --- /dev/null +++ b/src/cli/get_llrt.ts @@ -0,0 +1,38 @@ +import { resolve } from "node:path"; +import { writeFile } from "node:fs/promises"; +import { color, error } from "./console"; + +export async function getLlrt() { + const log = (text: string) => { + console.log(color(" Get LLRT ", "Blue"), text); + }; + + const config = await import("file://" + resolve("./lack.config.js")); + const path = config.llrt ?? "llrt.zip"; + + log("downloading llrt"); + const repoRes = await fetch( + "https://api.github.com/repos/awslabs/llrt/releases/latest" + ); + const data: { + assets: { + browser_download_url: string; + name: string; + }[]; + } = await repoRes.json(); + + const correctVersion = data.assets.find( + (x) => x.name === "llrt-lambda-arm64.zip" + ); + if (!correctVersion) { + error( + "Couldn't download the correct LLRT version, please set up Lack manually" + ); + return; + } + + const llrtBin = await fetch(correctVersion.browser_download_url); + const blob = await llrtBin.blob(); + await writeFile(path, Buffer.from(await blob.arrayBuffer())); + log("downloaded llrt"); +} diff --git a/src/cli/new_app.ts b/src/cli/new_app.ts new file mode 100644 index 0000000..962a07c --- /dev/null +++ b/src/cli/new_app.ts @@ -0,0 +1,67 @@ +import { color } from "./console"; +import { writeFile, mkdir } from "node:fs/promises"; +import { getLlrt } from "./get_llrt"; + +const sampleConfig = `export const name = "example-app" +export const llrt = "./llrt.zip" +`; + +const sampleGitignore = `./llrt.zip +out/ +node_modules/`; + +const samplePackage = { + type: "module", + name: "example-app", + version: "0.1.0", + devDependencies: { + lackFrw: "^0.1.0", + }, + scripts: { + dev: "lack-frw dev", + build: "lack-frw build", + deploy: "lack-frw deploy", + }, +}; + +// copied from bun init - might need to change +const sampleTsConfig = { + compilerOptions: { + lib: ["ESNext"], + target: "ESNext", + module: "ESNext", + moduleDetection: "force", + jsx: "react-jsx", + allowJs: true, + + moduleResolution: "bundler", + allowImportingTsExtensions: true, + verbatimModuleSyntax: true, + noEmit: true, + + strict: true, + skipLibCheck: true, + noFallthroughCasesInSwitch: true, + }, +}; + +export async function newApp() { + const log = (text: string) => { + console.log(color(" Create ", "Gray"), text); + }; + + await getLlrt(); + + log("creating new lack app"); + log("generating config, gitignore, package.json and tsconfig"); + await writeFile(".gitignore", sampleGitignore); + await writeFile("lack.config.js", sampleConfig); + await writeFile("package.json", JSON.stringify(samplePackage, undefined, 4)); + await writeFile( + "tsconfig.json", + JSON.stringify(sampleTsConfig, undefined, 4) + ); + + await mkdir("./app"); + log("done"); +} diff --git a/src/cli/parser.ts b/src/cli/parser.ts new file mode 100644 index 0000000..d3f0e46 --- /dev/null +++ b/src/cli/parser.ts @@ -0,0 +1,103 @@ +import { build } from "./build"; +import { error, warn } from "./console"; +import { dev } from "./dev"; +import { getLlrt } from "./get_llrt"; +import { newApp } from "./new_app"; +import { deploy } from "./deploy"; + +export type LogFunction = (...text: string[]) => void; +export type Command = { + title: string; + props: { name: string; required: boolean; hasValue: boolean }[]; + fn: () => any; +}; + +const commands: Command[] = [ + { + title: "deploy", + props: [], + fn: deploy, + }, + { + title: "build", + props: [], + fn: build, + }, + { + title: "dev", + props: [], + fn: dev, + }, + { + title: "create", + props: [], + fn: newApp, + }, + { + title: "get-llrt", + props: [], + fn: getLlrt, + }, +]; + +export function cliParser(args: string[]) { + args.splice(0, 2); // todo this might not the best idea + const commandName = args[0]; + if (!commands.map((x) => x.title).includes(commandName)) { + error("avalible commands are: ", ...commands.map((x) => `"${x.title}" `)); + return; + } + + // finish parsing command + args.splice(0, 1); + const command = commands.find((x) => x.title === commandName); + if (!command) throw new Error("couldnt find command"); + + // parse props + const props: { name: string; value?: string }[] = []; + for (const prop of args) { + // confrim proper formating of prop + if (!prop.startsWith("--")) { + error("props have to start with --"); + return; + } + const propSlice = prop.slice(2).split("="); + const setProp = command.props.find((x) => x.name === propSlice[0]); + if (!setProp) { + error( + "invalid prop, avalible ones are: ", + ...command.props.map((x) => `"${x.name}" `) + ); + return; + } + + // if the props is set as it needs a value + if (setProp.hasValue) { + if (propSlice.length !== 2) { + error( + `the prop ${setProp.name} has a value, please provide it by doing --prop=value` + ); + return; + } + + props.push({ name: setProp.name, value: propSlice[1] }); + continue; + } + if (propSlice.length === 2 && !setProp.hasValue) + warn( + `you passed a value to the prop "${setProp.name}" that actaully doesnt need it` + ); + + props.push({ name: setProp.name }); + } + + for (const required of command.props + .filter((x) => x.required) + .map((x) => x.name)) { + if (!props.map((x) => x.name).includes(required)) { + error(`the prop "${required}" needs to be set`); + return; + } + } + return { name: command.title, props, fn: command.fn } as const; +} diff --git a/src/deploy.ts b/src/deploy.ts index e0d5ef2..5726fdd 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -23,9 +23,6 @@ const methods = { any: aws_apigatewayv2.HttpMethod.ANY, }; -const random = () => - [...Array(5)].map(() => Math.random().toString(36)[2]).join(""); - export async function deploy() { const app = new App(); const userCdk = await import(join(process.cwd(), "./lack.config.js")); diff --git a/src/dev.ts b/src/dev.ts index ccc8674..f9148e5 100644 --- a/src/dev.ts +++ b/src/dev.ts @@ -1,6 +1,6 @@ import { getAllRoutes } from "./utils"; import { Hono } from "hono"; -import { LogFunction } from "./cli"; +import { LogFunction } from "./cli/parser"; import { serve } from "@hono/node-server"; import { resolve } from "path"; import { base } from "./consts";