From 282f6183866779f38ce88ffafe71a1e8be7dfe4c Mon Sep 17 00:00:00 2001 From: fedyna-k Date: Mon, 20 Jan 2025 08:31:12 +0100 Subject: [PATCH 1/6] Patched DB creation and added CLI toolchain base --- databases/ensure.ts | 2 ++ deno.json | 1 + toolbox/cli.ts | 60 +++++++++++++++++++++++++++++++++++++++++++++ toolbox/module.ts | 3 +++ 4 files changed, 66 insertions(+) create mode 100644 toolbox/cli.ts create mode 100644 toolbox/module.ts diff --git a/databases/ensure.ts b/databases/ensure.ts index 993fba3..1c56375 100644 --- a/databases/ensure.ts +++ b/databases/ensure.ts @@ -1,6 +1,8 @@ import { Database } from "@db/sqlite"; export default async function ensureDatabases() { + await Deno.mkdir("databases/data", { recursive: true }); + for await (const file of Deno.readDir("databases/init")) { if (!file.isFile) { console.warn(`[WARN] Path ${file.name} is not a file.`); diff --git a/deno.json b/deno.json index 9061960..7c0fd6e 100644 --- a/deno.json +++ b/deno.json @@ -25,6 +25,7 @@ "@db/sqlite": "jsr:@db/sqlite@^0.12.0", "@melvdouc/xml-parser": "jsr:@melvdouc/xml-parser@^0.1.1", "@popov/jwt": "jsr:@popov/jwt@^1.0.1", + "@std/cli": "jsr:@std/cli@^1.0.10", "preact": "https://esm.sh/preact@10.22.0", "preact/": "https://esm.sh/preact@10.22.0/", "@preact/signals": "https://esm.sh/*@preact/signals@1.2.2", diff --git a/toolbox/cli.ts b/toolbox/cli.ts new file mode 100644 index 0000000..4b23d84 --- /dev/null +++ b/toolbox/cli.ts @@ -0,0 +1,60 @@ +import { parseArgs, type ParseOptions } from "@std/cli/parse-args"; +import { createModule } from "$root/toolbox/module.ts"; + +interface CLI { + [command: string]: CLI | (() => void) | (() => Promise); +} + +const argSpec: ParseOptions = {}; + +const cli: CLI = { + help: () => displayHelp(cli), + module: { + create: createModule, + }, +}; + +function displayHelp(cli: CLI, errorMessage?: string): never { + const loggingFunction = errorMessage ? console.error : console.log; + if (errorMessage) { + console.error(errorMessage); + } + + loggingFunction("Commands:"); + +} + +function runCommand(commands: Array, cli: CLI): never | void | Promise { + if (commands.length == 0) { + console.error( + `No command provided. Available commands are ${ + JSON.stringify(Object.keys(cli)) + }.`, + ); + Deno.exit(1); + } + + const command = commands.shift()!.toString(); + + if (cli[command] == undefined) { + console.error( + `Command "${command}" doesn't exist. Available commands are ${ + JSON.stringify(Object.keys(cli)) + }.`, + ); + Deno.exit(1); + } + + if (typeof cli[command] == "object") { + return runCommand(commands, cli[command]); + } + + return cli[command](); +} + +function main() { + const args = parseArgs(Deno.args, argSpec); + runCommand(args._, cli); +} + +main(); diff --git a/toolbox/module.ts b/toolbox/module.ts new file mode 100644 index 0000000..6d8f455 --- /dev/null +++ b/toolbox/module.ts @@ -0,0 +1,3 @@ +export async function createModule() { + +} \ No newline at end of file From 9fee9ea0e88f03615423b2d7d7fca4969241c4e8 Mon Sep 17 00:00:00 2001 From: Kevin FEDYNA Date: Mon, 20 Jan 2025 16:44:52 +0100 Subject: [PATCH 2/6] Pending changes to add API example --- deno.json | 1 + fresh.gen.ts | 2 + routes/(apps)/notes/api/example.ts | 10 +++ toolbox/cli.ts | 86 +++++++++++++++----- toolbox/module.ts | 3 - toolbox/module/create.ts | 125 +++++++++++++++++++++++++++++ toolbox/module/list.ts | 7 ++ 7 files changed, 210 insertions(+), 24 deletions(-) create mode 100644 routes/(apps)/notes/api/example.ts delete mode 100644 toolbox/module.ts create mode 100644 toolbox/module/create.ts create mode 100644 toolbox/module/list.ts diff --git a/deno.json b/deno.json index 7c0fd6e..e9dcaca 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,7 @@ "tasks": { "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx", "cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -", + "pmpr": "deno run -A toolbox/cli.ts", "manifest": "deno task cli manifest $(pwd)", "start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts", "build": "deno run -A --unstable-ffi dev.ts build", diff --git a/fresh.gen.ts b/fresh.gen.ts index 62756f9..fbb4408 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -5,6 +5,7 @@ import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx"; import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx"; +import * as $_apps_notes_api_example from "./routes/(apps)/notes/api/example.ts"; import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx"; import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx"; import * as $_apps_notes_partials_admin_students from "./routes/(apps)/notes/partials/(admin)/students.tsx"; @@ -28,6 +29,7 @@ const manifest = { "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, "./routes/(apps)/mobility/partials/index.tsx": $_apps_mobility_partials_index, + "./routes/(apps)/notes/api/example.ts": $_apps_notes_api_example, "./routes/(apps)/notes/index.tsx": $_apps_notes_index, "./routes/(apps)/notes/partials/(admin)/courses.tsx": $_apps_notes_partials_admin_courses, diff --git a/routes/(apps)/notes/api/example.ts b/routes/(apps)/notes/api/example.ts new file mode 100644 index 0000000..68399cd --- /dev/null +++ b/routes/(apps)/notes/api/example.ts @@ -0,0 +1,10 @@ +import { Handlers } from "$fresh/server.ts"; + +export const handler: Handlers = { + async GET(request, context) { + return new Response({ + test: await request.json(), + context, + }); + }, +}; diff --git a/toolbox/cli.ts b/toolbox/cli.ts index 4b23d84..91f91f1 100644 --- a/toolbox/cli.ts +++ b/toolbox/cli.ts @@ -1,8 +1,11 @@ import { parseArgs, type ParseOptions } from "@std/cli/parse-args"; -import { createModule } from "$root/toolbox/module.ts"; +import { createModule } from "$root/toolbox/module/create.ts"; +import { listModules } from "$root/toolbox/module/list.ts"; +type CLICommand = (...args: Array) => void; +type CLIAsyncCommand = (...args: Array) => Promise; interface CLI { - [command: string]: CLI | (() => void) | (() => Promise); + [command: string]: CLI | CLICommand | CLIAsyncCommand; } const argSpec: ParseOptions = {}; @@ -10,51 +13,92 @@ const argSpec: ParseOptions = {}; const cli: CLI = { help: () => displayHelp(cli), module: { - create: createModule, + list: () => listModules(), + create: (name: string) => createModule(name), }, }; -function displayHelp(cli: CLI, errorMessage?: string): never { +function displayHelp( + cli: CLI | CLICommand | CLIAsyncCommand, + errorMessage?: string, +): never { const loggingFunction = errorMessage ? console.error : console.log; if (errorMessage) { console.error(errorMessage); } - loggingFunction("Commands:"); + if (typeof cli == "function") { + loggingFunction("Usage:"); + displayFunctionHelp(cli, loggingFunction); + } else { + loggingFunction("Commands:"); + displayObjectHelp(cli, loggingFunction); + } + Deno.exit(errorMessage ? 1 : 0); } -function runCommand(commands: Array, cli: CLI): never | void | Promise { +function displayObjectHelp( + cli: CLI, + loggingFunction: typeof console.log, + level: number = 1, +) { + for (const [key, value] of Object.entries(cli)) { + if (typeof value == "function") { + displayFunctionHelp(value, loggingFunction, level); + } else { + loggingFunction(`${" ".repeat(level * 2)}${key}`); + displayObjectHelp(value, loggingFunction, level + 1); + } + } +} + +function displayFunctionHelp( + cli: CLICommand | CLIAsyncCommand, + loggingFunction: typeof console.log, + level: number = 1, +) { + const command = cli.name; + const matched = cli.toString().match(/(?<=^\()[^\)]+(?=\))/); + const args = matched?.[0].split(",").map((arg) => `<${arg.trim()}>`); + loggingFunction( + `${" ".repeat(level * 2)}${command} ${args?.join(" ") ?? ""}`, + ); +} + +function runCommand( + commands: Array, + cli: CLI, +): never | { + command: CLICommand | CLIAsyncCommand; + args: Array; +} { if (commands.length == 0) { - console.error( - `No command provided. Available commands are ${ - JSON.stringify(Object.keys(cli)) - }.`, - ); - Deno.exit(1); + displayHelp(cli, `No command provided.`); } const command = commands.shift()!.toString(); if (cli[command] == undefined) { - console.error( - `Command "${command}" doesn't exist. Available commands are ${ - JSON.stringify(Object.keys(cli)) - }.`, - ); - Deno.exit(1); + displayHelp(cli, `Command "${command}" doesn't exist.`); } if (typeof cli[command] == "object") { return runCommand(commands, cli[command]); } - return cli[command](); + if (cli[command].length != commands.length) { + displayHelp(cli[command], `Wrong usage of command "${command}".`); + } + + return { command: cli[command], args: commands }; } function main() { - const args = parseArgs(Deno.args, argSpec); - runCommand(args._, cli); + const argv = parseArgs(Deno.args, argSpec); + const { command, args } = runCommand(argv._, cli); + + command(...args.map((element) => element.toString())); } main(); diff --git a/toolbox/module.ts b/toolbox/module.ts deleted file mode 100644 index 6d8f455..0000000 --- a/toolbox/module.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function createModule() { - -} \ No newline at end of file diff --git a/toolbox/module/create.ts b/toolbox/module/create.ts new file mode 100644 index 0000000..0d9f2b0 --- /dev/null +++ b/toolbox/module/create.ts @@ -0,0 +1,125 @@ +export async function createModule(name: string): Promise { + console.log(`Checking for module ${name}...`); + + try { + await Deno.mkdir(`routes/(apps)/${name}`); + } catch (error) { + if (!(error instanceof Deno.errors.AlreadyExists)) { + throw error; + } + + console.error(`Some module is already named ${name}, aborting.`); + Deno.exit(1); + } + + const capitalizedName = `${name[0].toUpperCase()}${ + name.substring(1).toLowerCase() + }`; + + Promise.allSettled([ + createDir(`routes/(apps)/${name}/(_props)`), + createDir(`routes/(apps)/${name}/partials`), + createDir(`routes/(apps)/${name}/(_islands)`), + createDir(`routes/(apps)/${name}/(_components)`), + createDir(`routes/(apps)/${name}/api`), + ]); + + Promise.allSettled([ + createFile( + `routes/(apps)/${name}/index.tsx`, + getIndexContent(capitalizedName), + ), + createFile( + `routes/(apps)/${name}/(_props)/props.ts`, + getPropsContent(capitalizedName), + ), + createFile( + `routes/(apps)/${name}/partials/index.tsx`, + getPartialIndexContent(capitalizedName), + ), + createFile( + `routes/(apps)/${name}/api/example.ts`, + getApiExampleContent(capitalizedName), + ), + ]); + + const formatter = new Deno.Command(Deno.execPath(), { + args: [ + "fmt", + `routes/(apps)/${name}`, + ], + }); + formatter.output(); +} + +function createFile(path: string, content: string): Promise { + console.log(`Creating file ${path}...`); + return Deno.writeTextFile(path, content); +} + +function createDir(path: string): Promise { + console.log(`Creating directory ${path}...`); + return Deno.mkdir(path); +} + +function getIndexContent(_name: string) { + return ` + import makeIndex from "$root/defaults/makeIndex.ts"; + export default makeIndex(import.meta.dirname!); + `; +} + +function getPartialIndexContent(name: string) { + return ` + import { EmptyObject } from "$root/defaults/interfaces.ts"; + import { + getPartialsConfig, + makePartials, + } from "$root/defaults/makePartials.tsx"; + + type ${name}IndexProps = EmptyObject; + + export function Index(_props: ${name}IndexProps) { + return

Welcome to ${name}.

; + } + + export const config = getPartialsConfig(); + export default makePartials(Index); + `; +} + +function getPropsContent(name: string) { + return ` + import { AppProperties } from "$root/defaults/interfaces.ts"; + + const properties: AppProperties = { + name: "${name}", + icon: "school", + pages: { + index: "Homepage", + }, + adminOnly: [], + hint: "PolyMPR module", + }; + + export default properties; + `; +} + +function getApiExampleContent(name: string) { + return ` + import { AppProperties } from "$root/defaults/interfaces.ts"; + + const properties: AppProperties = { + name: "${name}", + icon: "school", + pages: { + index: "Homepage", + }, + adminOnly: [], + hint: "PolyMPR module", + }; + + export default properties; + `; +} diff --git a/toolbox/module/list.ts b/toolbox/module/list.ts new file mode 100644 index 0000000..ce38149 --- /dev/null +++ b/toolbox/module/list.ts @@ -0,0 +1,7 @@ +export async function listModules(): Promise { + for await (const path of Deno.readDir("routes/(apps)")) { + if (path.isDirectory) { + console.log(path.name); + } + } +} \ No newline at end of file From f3c0b91f9c97abdd8247c4c4e2760867735df44d Mon Sep 17 00:00:00 2001 From: fedyna-k Date: Tue, 21 Jan 2025 09:19:50 +0100 Subject: [PATCH 3/6] Added CLI --- fresh.gen.ts | 2 -- routes/(apps)/notes/api/example.ts | 10 ------- toolbox/module/create.ts | 43 ++++++++++++++++++++---------- toolbox/module/list.ts | 2 +- 4 files changed, 30 insertions(+), 27 deletions(-) delete mode 100644 routes/(apps)/notes/api/example.ts diff --git a/fresh.gen.ts b/fresh.gen.ts index fbb4408..62756f9 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -5,7 +5,6 @@ import * as $_apps_layout from "./routes/(apps)/_layout.tsx"; import * as $_apps_mobility_index from "./routes/(apps)/mobility/index.tsx"; import * as $_apps_mobility_partials_index from "./routes/(apps)/mobility/partials/index.tsx"; -import * as $_apps_notes_api_example from "./routes/(apps)/notes/api/example.ts"; import * as $_apps_notes_index from "./routes/(apps)/notes/index.tsx"; import * as $_apps_notes_partials_admin_courses from "./routes/(apps)/notes/partials/(admin)/courses.tsx"; import * as $_apps_notes_partials_admin_students from "./routes/(apps)/notes/partials/(admin)/students.tsx"; @@ -29,7 +28,6 @@ const manifest = { "./routes/(apps)/mobility/index.tsx": $_apps_mobility_index, "./routes/(apps)/mobility/partials/index.tsx": $_apps_mobility_partials_index, - "./routes/(apps)/notes/api/example.ts": $_apps_notes_api_example, "./routes/(apps)/notes/index.tsx": $_apps_notes_index, "./routes/(apps)/notes/partials/(admin)/courses.tsx": $_apps_notes_partials_admin_courses, diff --git a/routes/(apps)/notes/api/example.ts b/routes/(apps)/notes/api/example.ts deleted file mode 100644 index 68399cd..0000000 --- a/routes/(apps)/notes/api/example.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Handlers } from "$fresh/server.ts"; - -export const handler: Handlers = { - async GET(request, context) { - return new Response({ - test: await request.json(), - context, - }); - }, -}; diff --git a/toolbox/module/create.ts b/toolbox/module/create.ts index 0d9f2b0..f289d98 100644 --- a/toolbox/module/create.ts +++ b/toolbox/module/create.ts @@ -1,4 +1,9 @@ export async function createModule(name: string): Promise { + if (!name.match(/^[a-zA-Z0-9](?:(?:\-(?!\-))?[a-zA-Z0-9]*)*[a-zA-Z0-9]$/)) { + console.error("Module names must be in kebab case."); + Deno.exit(1); + } + console.log(`Checking for module ${name}...`); try { @@ -12,9 +17,10 @@ export async function createModule(name: string): Promise { Deno.exit(1); } - const capitalizedName = `${name[0].toUpperCase()}${ - name.substring(1).toLowerCase() - }`; + const capitalizedName = name.match(/(^\w)|(\-\w)/g)!.reduce( + (word, pattern) => word.replace(pattern, pattern.at(-1)!.toUpperCase()), + name, + ); Promise.allSettled([ createDir(`routes/(apps)/${name}/(_props)`), @@ -106,20 +112,29 @@ function getPropsContent(name: string) { `; } -function getApiExampleContent(name: string) { +function getApiExampleContent(_name: string) { return ` - import { AppProperties } from "$root/defaults/interfaces.ts"; + import { Handlers } from "$fresh/server.ts"; - const properties: AppProperties = { - name: "${name}", - icon: "school", - pages: { - index: "Homepage", + export const handler: Handlers = { + async POST(request, context) { + if (request.headers.get("content-type") != "application/json") { + return new Response(null, { + status: 400 + }); + } + + const responseBody = { + requestBody: await request.json(), + context, + }; + + return new Response(JSON.stringify(responseBody), { + headers: { + "content-type": "application/json", + }, + }); }, - adminOnly: [], - hint: "PolyMPR module", }; - - export default properties; `; } diff --git a/toolbox/module/list.ts b/toolbox/module/list.ts index ce38149..a348047 100644 --- a/toolbox/module/list.ts +++ b/toolbox/module/list.ts @@ -4,4 +4,4 @@ export async function listModules(): Promise { console.log(path.name); } } -} \ No newline at end of file +} From 1d50b4d1b6814fe49f85427d6295bf17751ac40e Mon Sep 17 00:00:00 2001 From: fedyna-k Date: Tue, 21 Jan 2025 10:04:45 +0100 Subject: [PATCH 4/6] App running in compose --- Dockerfile | 12 ++++++++++++ compose.yml | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 Dockerfile create mode 100644 compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b88fc86 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM denoland/deno:alpine + +WORKDIR /app + +COPY . . +RUN deno cache main.ts +RUN deno task build + +USER deno +EXPOSE 443 + +CMD ["run", "-A", "main.ts"] \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..d23fe52 --- /dev/null +++ b/compose.yml @@ -0,0 +1,9 @@ +services: + app: + container_name: deno_fresh_app + build: . + ports: + - "443:443" + volumes: + - .:/app + command: deno run -A main.ts From 6e59515b4b6fd125311319da50029039267fc630 Mon Sep 17 00:00:00 2001 From: Kevin FEDYNA Date: Tue, 21 Jan 2025 13:31:38 +0100 Subject: [PATCH 5/6] Patch to add HTTP (auto redirected by Deno) --- Dockerfile | 1 + compose.yml | 1 + fresh.config.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b88fc86..fd8f30c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN deno cache main.ts RUN deno task build USER deno +EXPOSE 80 EXPOSE 443 CMD ["run", "-A", "main.ts"] \ No newline at end of file diff --git a/compose.yml b/compose.yml index d23fe52..530640a 100644 --- a/compose.yml +++ b/compose.yml @@ -3,6 +3,7 @@ services: container_name: deno_fresh_app build: . ports: + - "80:80" - "443:443" volumes: - .:/app diff --git a/fresh.config.ts b/fresh.config.ts index 5881650..7a518e1 100644 --- a/fresh.config.ts +++ b/fresh.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ server: { cert: await Deno.readTextFile("certs/cert.pem"), key: await Deno.readTextFile("certs/key.pem"), - port: 443, + port: 443 }, }); From 9fe64d5d7abb988a8aa49e8fb171e844e933c231 Mon Sep 17 00:00:00 2001 From: Kevin FEDYNA Date: Tue, 21 Jan 2025 13:33:20 +0100 Subject: [PATCH 6/6] Redo formatting --- fresh.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fresh.config.ts b/fresh.config.ts index 7a518e1..5881650 100644 --- a/fresh.config.ts +++ b/fresh.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ server: { cert: await Deno.readTextFile("certs/cert.pem"), key: await Deno.readTextFile("certs/key.pem"), - port: 443 + port: 443, }, });