diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c6d2a93..de95c0c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,5 +1,5 @@ /** @type {import("eslint").Linter.Config} */ -const config = { +module.exports = { root: true, parser: "@typescript-eslint/parser", plugins: ["isaacscript", "import"], @@ -12,32 +12,29 @@ const config = { ecmaVersion: "latest", sourceType: "module", tsconfigRootDir: __dirname, - project: [ - "./tsconfig.json", - "./cli/tsconfig.eslint.json", // separate eslint config for the CLI since we want to lint and typecheck differently due to template files - "./upgrade/tsconfig.json", - "./www/tsconfig.json", - ], + project: ["./tsconfig.json"], }, - overrides: [ - // Template files don't have reliable type information - { - files: ["./cli/template/**/*.{ts,tsx}"], - extends: ["plugin:@typescript-eslint/disable-type-checked"], - }, - ], + overrides: [], rules: { // These off/not-configured-the-way-we-want lint rules we like & opt into "@typescript-eslint/no-explicit-any": "error", + // Note: you must disable the base rule as it can report incorrect errors + "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ - "error", - { argsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" }, + "warn", + { + argsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, ], "@typescript-eslint/consistent-type-imports": [ "error", { prefer: "type-imports", fixStyle: "inline-type-imports" }, ], - "import/consistent-type-specifier-style": ["error", "prefer-inline"], + "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "@typescript-eslint/consistent-type-definitions": ["error", "type"], // For educational purposes we format our comments/jsdoc nicely "isaacscript/complete-sentences-jsdoc": "warn", @@ -51,5 +48,3 @@ const config = { "@typescript-eslint/prefer-nullish-coalescing": "off", }, }; - -module.exports = config; diff --git a/README.md b/README.md index 8854caf..cf55c46 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # This project was created using `create-beth-app` + ## To open an issue: https://github.com/ethanniser/the-beth-stack + ## To discuss: https://discord.gg/Z3yUtMfkwa ### To run locally: @@ -22,4 +24,4 @@ 3. Run `fly secrets set =` (probably want to set `NODE_ENV` to `"production"`) -5. Run `fly deploy` \ No newline at end of file +4. Run `fly deploy` diff --git a/bun.lockb b/bun.lockb index 2d9e96c..a2b8e34 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..8f3c940 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["bun-postcss-plugin"] diff --git a/drizzle.config.ts b/drizzle.config.ts index 45ef1dc..2ee0a6c 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,6 @@ import type { Config } from "drizzle-kit"; import { config } from "./src/config"; - const dbCredentials = { url: config.env.DATABASE_URL, authToken: config.env.DATABASE_AUTH_TOKEN!, @@ -14,4 +13,4 @@ export default { verbose: true, strict: true, tablesFilter: ["!libsql_wasm_func_table"], -} satisfies Config; \ No newline at end of file +} satisfies Config; diff --git a/package.json b/package.json index 4a6c1d6..ce77523 100644 --- a/package.json +++ b/package.json @@ -3,48 +3,51 @@ "module": "src/main.ts", "type": "module", "scripts": { - "dev": "concurrently \"bun run --hot src/main.ts\" \"bun run uno:dev\" \"bun run liveReload\"", + "dev": "concurrently \"bun run --hot src/main.ts\" \"bun run liveReload\"", "liveReload": "bunx beth-stack", - "start": "bun run uno && bun run src/main.ts", + "start": "bun run src/main.ts", "db:push": "bunx drizzle-kit push:sqlite", "db:studio": "bunx drizzle-kit studio", "db:seed": "bun run src/model/store/seed.ts", - "uno": "bunx --bun unocss", - "uno:dev": "bunx --bun unocss --watch", "typecheck": "bunx --bun tsc", "format:check": "prettier --check .", "format": "prettier --write . --list-different", "lint": "eslint . --report-unused-disable-directives", "lint:fix": "eslint . --report-unused-disable-directives --fix" }, + "postcss": { + "plugins": { + "tailwindcss": {} + } + }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.1.0", "@kitajs/ts-html-plugin": "^1.0.1", "@total-typescript/ts-reset": "^0.5.1", - "@unocss/transformer-variant-group": "^0.55.7", + "@types/eslint": "^8.44.2", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", "better-sqlite3": "^8.6.0", + "bun-postcss-plugin": "^1.0.1", "bun-types": "latest", "concurrently": "^8.2.1", "drizzle-kit": "^0.19.13", - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "eslint-config-turbo": "^1.10.14", "eslint-plugin-import": "^2.28.1", "eslint-plugin-isaacscript": "^3.5.5", "eslint-plugin-prettier": "^5.0.0", - "@types/eslint": "^8.44.2", "pino": "^8.15.1", "prettier": "^3.0.3", "prettier-plugin-tailwindcss": "^0.5.4", - "typescript": "^5.2.2", - "unocss": "^0.55.7" + "tailwindcss": "0.0.0-insiders.614c7e2", + "typescript": "^5.2.2" }, "dependencies": { - "@bogeychan/elysia-logger": "0.0.9", + "@bogeychan/elysia-logger": "0.0.11", "@elysiajs/cron": "^0.6.0", - "@elysiajs/static": "^0.6.0", + "@elysiajs/static": "^0.7.1", "@elysiajs/swagger": "^0.6.2", "@iconify-json/logos": "^1.1.37", "@iconify-json/lucide": "^1.1.128", @@ -56,7 +59,7 @@ "beth-stack": "0.0.33", "drizzle-orm": "^0.28.6", "drizzle-typebox": "^0.1.1", - "elysia": "0.7.4", + "elysia": "0.7.15", "lucia": "^2.6.0", "pino-pretty": "^10.2.0", "zod": "^3.22.2" diff --git a/prettier.config.cjs b/prettier.config.cjs index 74e9d1f..800b8a2 100644 --- a/prettier.config.cjs +++ b/prettier.config.cjs @@ -1,17 +1,7 @@ -/** @typedef {import("prettier").Config} PrettierConfig */ - -/** @type { PrettierConfig | SortImportsConfig } */ -const config = { - arrowParens: "always", - printWidth: 80, - singleQuote: false, - semi: true, - trailingComma: "all", - tabWidth: 2, +/** @type {import("prettier").Config} */ +module.exports = { plugins: [ "@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss", ], }; - -module.exports = config; diff --git a/public/.gitkeep b/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/reset.d.ts b/reset.d.ts index e186b1f..a3d4a03 100644 --- a/reset.d.ts +++ b/reset.d.ts @@ -1 +1 @@ -import "@total-typescript/ts-reset"; \ No newline at end of file +import "@total-typescript/ts-reset"; diff --git a/src/auth/index.ts b/src/auth/index.ts index 4746f0c..0fbff75 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,6 +1,7 @@ import { libsql } from "@lucia-auth/adapter-sqlite"; import { github } from "@lucia-auth/oauth/providers"; -import { lucia, Middleware } from "lucia"; +import { lucia } from "lucia"; +import type { Middleware } from "lucia"; import { config } from "../config"; import { client } from "../db"; diff --git a/src/components/base.tsx b/src/components/base.tsx index e83b167..58b188c 100644 --- a/src/components/base.tsx +++ b/src/components/base.tsx @@ -1,5 +1,5 @@ import { liveReloadScript } from "beth-stack/dev"; -import { type PropsWithChildren } from "beth-stack/jsx"; +import type { PropsWithChildren } from "beth-stack/jsx"; import { config } from "../config"; const safeScript = @@ -14,11 +14,7 @@ export const BaseHtml = ({ children }: PropsWithChildren) => ( - - + diff --git a/src/components/tweets.tsx b/src/components/tweets.tsx index 766dac6..6e99bde 100644 --- a/src/components/tweets.tsx +++ b/src/components/tweets.tsx @@ -1,5 +1,4 @@ import { db } from "../db"; -import { tweets } from "../db/schema/tweets"; export function TweetCard({ author: { handle }, diff --git a/src/context/index.ts b/src/context/index.ts index 0dab923..7de1de6 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -1,5 +1,4 @@ import { logger } from "@bogeychan/elysia-logger"; -import { Logger } from "@bogeychan/elysia-logger/types"; import { cron } from "@elysiajs/cron"; import { HoltLogger } from "@tlscipher/holt"; import { bethStack } from "beth-stack/elysia"; @@ -30,13 +29,13 @@ export const ctx = new Elysia({ .use(bethStack()) .use(logger(loggerConfig)) .use( - // @ts-expect-error + // @ts-expect-error missing toString symbol config.env.NODE_ENV === "development" ? new HoltLogger().getLogger() : (a) => a, ) .use( - // @ts-expect-error + // @ts-expect-error missing toString symbol config.env.DATABASE_CONNECTION_TYPE === "local-replica" ? cron({ name: "heartbeat", @@ -66,7 +65,7 @@ export const ctx = new Elysia({ log.debug(`Request received: ${request.method}: ${request.url}`); } }) - .onResponse(({ log, request, set }) => { + .onResponse(({ log, request }) => { if (log && config.env.NODE_ENV === "production") { log.debug(`Response sent: ${request.method}: ${request.url}`); } diff --git a/src/controllers/auth.tsx b/src/controllers/auth.tsx index 6c5be98..52c662c 100644 --- a/src/controllers/auth.tsx +++ b/src/controllers/auth.tsx @@ -30,8 +30,13 @@ export const authController = new Elysia({ handle, }, }) - .catch((err) => { - if (err.code === "SQLITE_CONSTRAINT") { + .catch((err: unknown) => { + if ( + typeof err === "object" && + err && + "code" in err && + err.code === "SQLITE_CONSTRAINT" + ) { throw new DuplicateEmailError(); } else { throw err; @@ -68,7 +73,6 @@ export const authController = new Elysia({ }), // Enum to validate action type }), error({ code, error, set, log }) { - log.error(error); let errorMessage = ""; diff --git a/src/controllers/tweets.tsx b/src/controllers/tweets.tsx index 163695c..960f3ec 100644 --- a/src/controllers/tweets.tsx +++ b/src/controllers/tweets.tsx @@ -1,6 +1,5 @@ import { eq } from "drizzle-orm"; import { Elysia, t } from "elysia"; -import { authed } from "../auth/middleware"; import { AdditionalTweetList, TweetCard } from "../components/tweets"; import { ctx } from "../context"; import { tweets } from "../db/schema/tweets"; @@ -10,10 +9,12 @@ export const tweetsController = new Elysia({ }) .use(ctx) .derive(async (ctx) => { - const authRequest = ctx.auth.handleRequest(ctx); - const session = await authRequest.validate(); + if (ctx.request) { + const authRequest = ctx.auth.handleRequest(ctx); + const session = await authRequest.validate(); - return { session }; + return { session }; + } }) .get( "/", @@ -32,7 +33,7 @@ export const tweetsController = new Elysia({ ) .post( "/", - async ({ session, db, body, set, log }) => { + async ({ session, db, body, set }) => { if (!session) { set.status = "Unauthorized"; set.headers["HX-Redirect"] = "/signin"; diff --git a/src/db/index.ts b/src/db/index.ts index be7b2fa..06e38e0 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,4 +1,5 @@ -import { createClient, type Config } from "@libsql/client"; +import { createClient } from "@libsql/client"; +import type { Config } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; import { config } from "../config"; import * as schema from "./schema"; diff --git a/src/db/schema/tweets.ts b/src/db/schema/tweets.ts index 366597b..a033a91 100644 --- a/src/db/schema/tweets.ts +++ b/src/db/schema/tweets.ts @@ -1,6 +1,5 @@ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { createInsertSchema, createSelectSchema } from "drizzle-typebox"; -import { user } from "."; export const tweets = sqliteTable( "tweet", diff --git a/src/main.ts b/src/main.ts index 8d3efb7..e6bb87a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,27 +1,29 @@ import { staticPlugin } from "@elysiajs/static"; -// import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { config } from "./config"; -import { ctx } from "./context"; import { api } from "./controllers/*"; import { pages } from "./pages/*"; +import styles from "./styles.css"; const app = new Elysia() // .use(swagger()) - // @ts-expect-error .use(staticPlugin()) .use(api) .use(pages) + .get("/styles.css", ({ set }) => { + set.headers["Content-Type"] = "text/css"; + return styles; + }) .onStart(({ log }) => { if (config.env.NODE_ENV === "development") { void fetch("http://localhost:3001/restart"); - // log.debug("🦊 Triggering Live Reload"); - console.log("🦊 Triggering Live Reload"); + log.debug("🦊 Triggering Live Reload"); + // console.log("🦊 Triggering Live Reload"); } }) .onError(({ code, error, request, log }) => { - // log.error(` ${request.method} ${request.url}`, code, error); - console.error(error); + log.error(` ${request.method} ${request.url}`, code, error); + // console.error(error); }) .listen(3000); diff --git a/src/pages/(auth)/signin.tsx b/src/pages/(auth)/signin.tsx index 4da0da0..4c06b0d 100644 --- a/src/pages/(auth)/signin.tsx +++ b/src/pages/(auth)/signin.tsx @@ -111,57 +111,54 @@ export const login = new Elysia() set.headers["Set-Cookie"] = stateCookie; set.redirect = url.toString(); }) - .get( - "/login/github/callback", - async ({ request, log, path, query, set, auth }) => { - const { code, state } = query; + .get("/login/github/callback", async ({ request, log, query, set, auth }) => { + const { code, state } = query; - const cookies = parseCookie(request.headers.get("Cookie") ?? ""); - const storedState = cookies.github_oauth_state; + const cookies = parseCookie(request.headers.get("Cookie") ?? ""); + const storedState = cookies.github_oauth_state; - if (!storedState || !state || storedState !== state || !code) { - set.status = 400; - return "Invalid state"; - } - - try { - const { getExistingUser, githubUser, createUser } = - await githubAuth.validateCallback(code); + if (!storedState || !state || storedState !== state || !code) { + set.status = 400; + return "Invalid state"; + } - const getUser = async () => { - const existingUser = await getExistingUser(); - if (existingUser) return existingUser; - const user = await createUser({ - attributes: { - handle: githubUser.login, - }, - }); - return user; - }; + try { + const { getExistingUser, githubUser, createUser } = + await githubAuth.validateCallback(code); - const user = await getUser(); - const session = await auth.createSession({ - userId: user.userId, - attributes: {}, - }); - const sessionCookie = auth.createSessionCookie(session); - // redirect to profile page - return new Response(null, { - headers: { - Location: "/", - "Set-Cookie": sessionCookie.serialize(), // store session cookie + const getUser = async () => { + const existingUser = await getExistingUser(); + if (existingUser) return existingUser; + const user = await createUser({ + attributes: { + handle: githubUser.login, }, - status: 302, }); - } catch (e) { - if (e instanceof OAuthRequestError) { - // invalid code - set.status = 400; - return e.message; - } - set.status = 500; - log.error(e); - return "Internal server error"; + return user; + }; + + const user = await getUser(); + const session = await auth.createSession({ + userId: user.userId, + attributes: {}, + }); + const sessionCookie = auth.createSessionCookie(session); + // redirect to profile page + return new Response(null, { + headers: { + Location: "/", + "Set-Cookie": sessionCookie.serialize(), // store session cookie + }, + status: 302, + }); + } catch (e) { + if (e instanceof OAuthRequestError) { + // invalid code + set.status = 400; + return e.message; } - }, - ); + set.status = 500; + log.error(e); + return "Internal server error"; + } + }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ecfe08e..074667e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,6 +1,4 @@ -import { Suspense } from "beth-stack/jsx"; import { Elysia } from "elysia"; -import { authed } from "../auth/middleware"; import { BaseHtml } from "../components/base"; import { InitialTweetList, TweetCreationForm } from "../components/tweets"; import { ctx } from "../context"; @@ -8,12 +6,14 @@ import { ctx } from "../context"; export const index = new Elysia() .use(ctx) .derive(async (ctx) => { - const authRequest = ctx.auth.handleRequest(ctx); - const session = await authRequest.validate(); + if (ctx.request) { + const authRequest = ctx.auth.handleRequest(ctx); + const session = await authRequest.validate(); - return { session }; + return { session }; + } }) - .get("/", async ({ htmlStream, session, db }) => { + .get("/", ({ htmlStream, session }) => { return htmlStream(() => (
diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/styles.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/src/types/htmx.d.ts b/src/types/htmx.d.ts index 8faaae3..fe98948 100644 --- a/src/types/htmx.d.ts +++ b/src/types/htmx.d.ts @@ -1,4 +1,5 @@ type RoutesByType< + // eslint-disable-next-line @typescript-eslint/no-explicit-any Schema extends Record, // Ensure keys are strings Type extends "get" | "post" | "put" | "delete" | "patch", > = RouterPattern< @@ -20,22 +21,23 @@ type RemoveSlash = S extends `${infer T}/` : S; type RouterPattern = - T extends `${infer Start}:${infer Param}/${infer Rest}` + T extends `${infer Start}:${infer _}/${infer Rest}` ? `${Start}${string}/${RouterPattern}` - : T extends `${infer Start}:${infer Param}` + : T extends `${infer Start}:${infer _}` ? `${Start}${string}` : T extends `${infer Start}*` ? `${Start}${string}` : T; -type StartsWithApi = T extends `${"/api"}${infer Rest}` +type StartsWithApi = T extends `${"/api"}${infer _}` ? T : never; -type DoesntStartWithApi = T extends `${"/api"}${infer Rest}` +type DoesntStartWithApi = T extends `${"/api"}${infer _}` ? never : T; +// eslint-disable-next-line @typescript-eslint/consistent-type-imports type Schema = import("../main").App["schema"]; type PostRoutes = RoutesByType; @@ -44,9 +46,12 @@ type PutRoutes = RoutesByType; type DeleteRoutes = RoutesByType; type PatchRoutes = RoutesByType; +type AllowQuery = `${T}?${string}` | T; + declare namespace JSX { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface HtmlTag extends Htmx.Attributes { - ["hx-get"]?: StartsWithApi; + ["hx-get"]?: AllowQuery>; ["hx-post"]?: StartsWithApi; ["hx-put"]?: StartsWithApi; ["hx-delete"]?: StartsWithApi; diff --git a/src/types/lucia.d.ts b/src/types/lucia.d.ts index 3dd9fc2..6a21805 100644 --- a/src/types/lucia.d.ts +++ b/src/types/lucia.d.ts @@ -1,8 +1,9 @@ /// declare namespace Lucia { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports type Auth = import("../auth/index").Auth; type DatabaseUserAttributes = { handle: string; }; - type DatabaseSessionAttributes = {}; + type DatabaseSessionAttributes = object; } diff --git a/tsconfig.json b/tsconfig.json index a521d90..c2add4e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,7 @@ { + "$schema": "https://json.schemastore.org/tsconfig.json", "compilerOptions": { + "rootDir": ".", "lib": ["ESNext"], "module": "esnext", "target": "esnext", @@ -18,16 +20,15 @@ "forceConsistentCasingInFileNames": true, "allowJs": true, "types": [ - "bun-types" // add Bun global + "bun-types", // add Bun global + "bun-postcss-plugin" ], - // non bun init - // "plugins": [{ "name": "@kitajs/ts-html-plugin" }], "noUncheckedIndexedAccess": true, - // "noUnusedLocals": true, - // "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true - // "noImplicitReturns": true - } + }, + "files": [".eslintrc.cjs"], + "include": ["**/*", "**/*.d.ts"], + "exclude": ["./node_modules"] } diff --git a/uno.config.ts b/uno.config.ts deleted file mode 100644 index 9674a1e..0000000 --- a/uno.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import transformerVariantGroup from "@unocss/transformer-variant-group"; -import { defineConfig, presetIcons, presetWebFonts, presetWind } from "unocss"; - -export default defineConfig({ - cli: { - entry: { - patterns: ["src/**/*.{ts,tsx}"], - outFile: "public/dist/unocss.css", - }, - }, - presets: [presetWind(), presetIcons(), presetWebFonts()], - transformers: [transformerVariantGroup()], -});