create-pyro 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +32 -11
  2. package/dist/index.js +150 -30
  3. package/dist/scaffold.js +106 -0
  4. package/package.json +7 -7
  5. package/template/.dockerignore +30 -0
  6. package/template/.env.example +23 -0
  7. package/template/.gitattributes +2 -0
  8. package/template/.github/workflows/ci.yml +43 -0
  9. package/template/.vscode/extensions.json +3 -0
  10. package/template/.vscode/settings.json +8 -0
  11. package/template/README.md +57 -4
  12. package/template/apps/api/Dockerfile +46 -0
  13. package/template/apps/api/package.json +43 -0
  14. package/template/apps/api/src/app.test.ts +44 -0
  15. package/template/apps/api/src/app.ts +54 -0
  16. package/template/apps/api/src/index.ts +13 -0
  17. package/template/apps/api/src/lib/auth.ts +46 -0
  18. package/template/apps/api/src/lib/response.ts +28 -0
  19. package/template/apps/api/src/lib/session.ts +22 -0
  20. package/template/apps/api/src/middleware/auth.ts +34 -0
  21. package/template/apps/api/src/routes/auth.ts +9 -0
  22. package/template/apps/api/src/routes/index.ts +8 -0
  23. package/template/apps/api/src/routes/me.test.ts +45 -0
  24. package/template/apps/api/src/routes/me.ts +29 -0
  25. package/template/apps/api/src/test/database-url.ts +23 -0
  26. package/template/apps/api/src/test/global-setup.ts +36 -0
  27. package/template/apps/api/src/test/setup.ts +8 -0
  28. package/template/apps/api/src/types/context.ts +8 -0
  29. package/template/apps/api/tsconfig.build.json +8 -0
  30. package/template/apps/api/tsconfig.json +9 -0
  31. package/template/apps/api/tsdown.config.ts +13 -0
  32. package/template/apps/api/vitest.config.ts +11 -0
  33. package/template/apps/web/Dockerfile +49 -0
  34. package/template/apps/web/app/app.css +29 -0
  35. package/template/apps/web/app/lib/api.server.ts +13 -0
  36. package/template/apps/web/app/lib/api.test.ts +28 -0
  37. package/template/apps/web/app/lib/api.ts +47 -0
  38. package/template/apps/web/app/lib/auth-client.ts +13 -0
  39. package/template/apps/web/app/lib/env.server.ts +11 -0
  40. package/template/apps/web/app/lib/public-env.ts +17 -0
  41. package/template/apps/web/app/lib/session.server.ts +31 -0
  42. package/template/apps/web/app/lib/utils.ts +6 -0
  43. package/template/apps/web/app/root.tsx +86 -0
  44. package/template/apps/web/app/routes/dashboard.tsx +81 -0
  45. package/template/apps/web/app/routes/home.tsx +161 -0
  46. package/template/apps/web/app/routes.ts +6 -0
  47. package/template/apps/web/package.json +41 -0
  48. package/template/apps/web/public/favicon.svg +5 -0
  49. package/template/apps/web/react-router.config.ts +6 -0
  50. package/template/apps/web/tsconfig.json +23 -0
  51. package/template/apps/web/vite.config.ts +19 -0
  52. package/template/apps/web/vitest.config.ts +13 -0
  53. package/template/biome.json +53 -0
  54. package/template/compose.dev.yml +26 -0
  55. package/template/compose.yml +75 -0
  56. package/template/gitignore +32 -1
  57. package/template/infra/nginx/default.conf +33 -0
  58. package/template/package.json +23 -1
  59. package/template/packages/config/package.json +31 -0
  60. package/template/packages/config/src/index.test.ts +100 -0
  61. package/template/packages/config/src/index.ts +100 -0
  62. package/template/packages/config/tsconfig.build.json +8 -0
  63. package/template/packages/config/tsconfig.json +9 -0
  64. package/template/packages/config/tsdown.config.ts +12 -0
  65. package/template/packages/db/Dockerfile +22 -0
  66. package/template/packages/db/drizzle/0000_init.sql +53 -0
  67. package/template/packages/db/drizzle/meta/0000_snapshot.json +376 -0
  68. package/template/packages/db/drizzle/meta/_journal.json +13 -0
  69. package/template/packages/db/drizzle.config.ts +12 -0
  70. package/template/packages/db/package.json +38 -0
  71. package/template/packages/db/src/client.ts +12 -0
  72. package/template/packages/db/src/index.ts +2 -0
  73. package/template/packages/db/src/schema/auth.ts +96 -0
  74. package/template/packages/db/src/schema/index.ts +1 -0
  75. package/template/packages/db/tsconfig.build.json +7 -0
  76. package/template/packages/db/tsconfig.json +9 -0
  77. package/template/packages/db/tsdown.config.ts +12 -0
  78. package/template/packages/shared/package.json +32 -0
  79. package/template/packages/shared/src/index.ts +2 -0
  80. package/template/packages/shared/src/schemas/auth.test.ts +32 -0
  81. package/template/packages/shared/src/schemas/auth.ts +22 -0
  82. package/template/packages/shared/src/schemas/index.ts +1 -0
  83. package/template/packages/shared/src/types/api.ts +15 -0
  84. package/template/packages/shared/src/types/index.ts +2 -0
  85. package/template/packages/shared/src/types/user.ts +10 -0
  86. package/template/packages/shared/tsconfig.build.json +8 -0
  87. package/template/packages/shared/tsconfig.json +9 -0
  88. package/template/packages/shared/tsdown.config.ts +12 -0
  89. package/template/pnpm-workspace.yaml +25 -0
  90. package/template/tsconfig.json +19 -0
  91. package/template/turbo.json +27 -0
@@ -0,0 +1,47 @@
1
+ import type { ApiError } from "@repo/shared";
2
+
3
+ export class ApiRequestError extends Error {
4
+ code: string;
5
+ status: number;
6
+ details?: unknown;
7
+
8
+ constructor(
9
+ message: string,
10
+ code: string,
11
+ status: number,
12
+ details?: unknown,
13
+ ) {
14
+ super(message);
15
+ this.name = "ApiRequestError";
16
+ this.code = code;
17
+ this.status = status;
18
+ this.details = details;
19
+ }
20
+ }
21
+
22
+ type JsonResponse = { status: number; json: () => Promise<unknown> };
23
+
24
+ type DataOf<R> = R extends { json: () => Promise<infer J> }
25
+ ? J extends { success: true; data: infer D }
26
+ ? D
27
+ : never
28
+ : never;
29
+
30
+ /**
31
+ * Reads an api response and returns its `data`, typed from the hono client.
32
+ * Throws `ApiRequestError` for an `{ success: false }` body.
33
+ */
34
+ export async function unwrap<R extends JsonResponse>(
35
+ response: R,
36
+ ): Promise<DataOf<R>> {
37
+ const json = (await response.json()) as
38
+ | { success: true; data: unknown }
39
+ | ApiError;
40
+
41
+ if (!json.success) {
42
+ const { message, code, details } = json.error;
43
+ throw new ApiRequestError(message, code, response.status, details);
44
+ }
45
+
46
+ return json.data as DataOf<R>;
47
+ }
@@ -0,0 +1,13 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import { publicApiUrl } from "./public-env";
3
+
4
+ let client: ReturnType<typeof createAuthClient> | undefined;
5
+
6
+ /**
7
+ * The Better Auth client. Created on first use because it needs the browser's
8
+ * runtime config, so only call it from event handlers and effects.
9
+ */
10
+ export function authClient() {
11
+ client ??= createAuthClient({ baseURL: `${publicApiUrl()}/api/auth` });
12
+ return client;
13
+ }
@@ -0,0 +1,11 @@
1
+ import { webEnv } from "@repo/config";
2
+ import type { PublicEnv } from "./public-env";
3
+
4
+ export const serverEnv = webEnv;
5
+
6
+ /** The subset of the server's env that is safe to send to the browser. */
7
+ export function getPublicEnv(): PublicEnv {
8
+ return {
9
+ API_URL: webEnv.PUBLIC_API_URL,
10
+ };
11
+ }
@@ -0,0 +1,17 @@
1
+ // Runtime config for the browser. The root loader serializes it into
2
+ // `window.__ENV__`, so one built image works on any origin.
3
+
4
+ export type PublicEnv = {
5
+ API_URL: string;
6
+ };
7
+
8
+ declare global {
9
+ interface Window {
10
+ __ENV__?: Partial<PublicEnv>;
11
+ }
12
+ }
13
+
14
+ /** The origin the browser reaches the api on. Browser-only. */
15
+ export function publicApiUrl(): string {
16
+ return window.__ENV__?.API_URL ?? window.location.origin;
17
+ }
@@ -0,0 +1,31 @@
1
+ import { redirect } from "react-router";
2
+ import { serverEnv } from "./env.server";
3
+
4
+ export type Session = {
5
+ user: { id: string; name: string; email: string };
6
+ session: { id: string; expiresAt: string };
7
+ };
8
+
9
+ /** The request's session, or null when signed out or the api is unreachable. */
10
+ export async function getSession(request: Request): Promise<Session | null> {
11
+ try {
12
+ const res = await fetch(
13
+ `${serverEnv.INTERNAL_API_URL}/api/auth/get-session`,
14
+ {
15
+ headers: { cookie: request.headers.get("cookie") ?? "" },
16
+ },
17
+ );
18
+ if (!res.ok) return null;
19
+ const data = (await res.json()) as Session | null;
20
+ return data?.user ? data : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ /** The request's session; redirects to the homepage when signed out. */
27
+ export async function requireSession(request: Request): Promise<Session> {
28
+ const session = await getSession(request);
29
+ if (!session) throw redirect("/");
30
+ return session;
31
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,86 @@
1
+ import {
2
+ isRouteErrorResponse,
3
+ Links,
4
+ Meta,
5
+ Outlet,
6
+ Scripts,
7
+ ScrollRestoration,
8
+ useRouteLoaderData,
9
+ } from "react-router";
10
+ import type { Route } from "./+types/root";
11
+ import "./app.css";
12
+ import { getPublicEnv } from "./lib/env.server";
13
+
14
+ export function loader() {
15
+ return { env: getPublicEnv() };
16
+ }
17
+
18
+ export const links: Route.LinksFunction = () => [
19
+ { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
20
+ ];
21
+
22
+ /** Hands the browser its runtime config (see lib/public-env.ts). */
23
+ function PublicEnvScript() {
24
+ const data = useRouteLoaderData<typeof loader>("root");
25
+ const json = JSON.stringify(data?.env ?? {}).replace(/</g, "\\u003c");
26
+
27
+ return (
28
+ <script dangerouslySetInnerHTML={{ __html: `window.__ENV__=${json}` }} />
29
+ );
30
+ }
31
+
32
+ export function Layout({ children }: { children: React.ReactNode }) {
33
+ return (
34
+ <html lang="en">
35
+ <head>
36
+ <meta charSet="utf-8" />
37
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
38
+ <Meta />
39
+ <Links />
40
+ </head>
41
+ <body>
42
+ {children}
43
+ <ScrollRestoration />
44
+ <PublicEnvScript />
45
+ <Scripts />
46
+ </body>
47
+ </html>
48
+ );
49
+ }
50
+
51
+ export default function App() {
52
+ return <Outlet />;
53
+ }
54
+
55
+ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
56
+ let title = "Something went wrong";
57
+ let details = "An unexpected error occurred. Please try again in a moment.";
58
+ let stack: string | undefined;
59
+
60
+ if (isRouteErrorResponse(error)) {
61
+ if (error.status === 404) {
62
+ title = "Page not found";
63
+ details = "The page you are looking for does not exist.";
64
+ } else if (error.statusText) {
65
+ details = error.statusText;
66
+ }
67
+ } else if (import.meta.env.DEV && error instanceof Error) {
68
+ details = error.message;
69
+ stack = error.stack;
70
+ }
71
+
72
+ return (
73
+ <main className="mx-auto flex min-h-svh max-w-2xl flex-col justify-center gap-4 px-6 py-12">
74
+ <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
75
+ <p className="text-ink-muted">{details}</p>
76
+ <a href="/" className="font-medium text-accent hover:underline">
77
+ Back to home
78
+ </a>
79
+ {stack && (
80
+ <pre className="overflow-x-auto rounded-lg border border-line bg-surface-raised p-4 text-sm">
81
+ <code>{stack}</code>
82
+ </pre>
83
+ )}
84
+ </main>
85
+ );
86
+ }
@@ -0,0 +1,81 @@
1
+ import { useNavigate } from "react-router";
2
+ import { unwrap } from "~/lib/api";
3
+ import { api } from "~/lib/api.server";
4
+ import { authClient } from "~/lib/auth-client";
5
+ import { requireSession } from "~/lib/session.server";
6
+ import type { Route } from "./+types/dashboard";
7
+
8
+ export function meta() {
9
+ return [{ title: "Dashboard · my-pyro-app" }];
10
+ }
11
+
12
+ const formatDate = (iso: string) =>
13
+ new Intl.DateTimeFormat("en", {
14
+ dateStyle: "medium",
15
+ timeStyle: "short",
16
+ timeZone: "UTC",
17
+ }).format(new Date(iso));
18
+
19
+ export async function loader({ request }: Route.LoaderArgs) {
20
+ await requireSession(request);
21
+
22
+ // Typed end to end: the response shape comes from the api's route.
23
+ const user = await unwrap(await api(request).api.me.$get());
24
+
25
+ return {
26
+ user,
27
+ joined: `${formatDate(user.createdAt)} UTC`,
28
+ };
29
+ }
30
+
31
+ export default function Dashboard({ loaderData }: Route.ComponentProps) {
32
+ const { user, joined } = loaderData;
33
+ const navigate = useNavigate();
34
+
35
+ async function signOut() {
36
+ await authClient().signOut();
37
+ navigate("/", { replace: true });
38
+ }
39
+
40
+ const details = [
41
+ ["Name", user.name],
42
+ ["Email", user.email],
43
+ ["Email verified", user.emailVerified ? "Yes" : "No"],
44
+ ["User ID", user.id],
45
+ ["Joined", joined],
46
+ ] as const;
47
+
48
+ return (
49
+ <main className="mx-auto flex min-h-svh max-w-xl flex-col justify-center gap-8 px-6 py-12">
50
+ <header className="flex items-end justify-between gap-4">
51
+ <div className="flex flex-col gap-2">
52
+ <span className="font-mono text-xs uppercase tracking-widest text-accent">
53
+ Dashboard
54
+ </span>
55
+ <h1 className="text-3xl font-semibold tracking-tight">
56
+ Hi, {user.name}
57
+ </h1>
58
+ </div>
59
+ <button
60
+ type="button"
61
+ onClick={signOut}
62
+ className="rounded-lg border border-line px-3 py-1.5 text-sm font-medium transition hover:bg-surface-raised"
63
+ >
64
+ Sign out
65
+ </button>
66
+ </header>
67
+
68
+ <dl className="divide-y divide-line rounded-xl border border-line bg-surface-raised shadow-sm">
69
+ {details.map(([label, value]) => (
70
+ <div
71
+ key={label}
72
+ className="grid grid-cols-[8rem_1fr] gap-4 px-5 py-3 text-sm"
73
+ >
74
+ <dt className="text-ink-muted">{label}</dt>
75
+ <dd className="break-all font-medium">{value}</dd>
76
+ </div>
77
+ ))}
78
+ </dl>
79
+ </main>
80
+ );
81
+ }
@@ -0,0 +1,161 @@
1
+ import { type FormEvent, useState } from "react";
2
+ import { Link, useNavigate } from "react-router";
3
+ import { signInSchema, signUpSchema } from "@repo/shared";
4
+ import { authClient } from "~/lib/auth-client";
5
+ import { getSession } from "~/lib/session.server";
6
+ import { cn } from "~/lib/utils";
7
+ import type { Route } from "./+types/home";
8
+
9
+ export function meta() {
10
+ return [{ title: "my-pyro-app" }];
11
+ }
12
+
13
+ export async function loader({ request }: Route.LoaderArgs) {
14
+ const session = await getSession(request);
15
+ return { user: session?.user ?? null };
16
+ }
17
+
18
+ type Mode = "sign-in" | "sign-up";
19
+
20
+ export default function Home({ loaderData }: Route.ComponentProps) {
21
+ const { user } = loaderData;
22
+
23
+ return (
24
+ <main className="mx-auto flex min-h-svh max-w-sm flex-col justify-center gap-8 px-6 py-12">
25
+ <header className="flex flex-col gap-2">
26
+ <span className="font-mono text-xs uppercase tracking-widest text-accent">
27
+ my-pyro-app
28
+ </span>
29
+ <h1 className="text-3xl font-semibold tracking-tight">
30
+ {user ? `Welcome back, ${user.name}` : "Welcome"}
31
+ </h1>
32
+ <p className="text-ink-muted">
33
+ {user
34
+ ? "You're signed in."
35
+ : "Sign in, or create an account to get started."}
36
+ </p>
37
+ </header>
38
+
39
+ {user ? (
40
+ <Link
41
+ to="/dashboard"
42
+ className="rounded-lg bg-accent px-4 py-2.5 text-center font-medium text-white transition hover:bg-accent-strong"
43
+ >
44
+ Go to dashboard
45
+ </Link>
46
+ ) : (
47
+ <AuthForm />
48
+ )}
49
+ </main>
50
+ );
51
+ }
52
+
53
+ function AuthForm() {
54
+ const navigate = useNavigate();
55
+ const [mode, setMode] = useState<Mode>("sign-in");
56
+ const [error, setError] = useState<string | null>(null);
57
+ const [pending, setPending] = useState(false);
58
+
59
+ async function onSubmit(event: FormEvent<HTMLFormElement>) {
60
+ event.preventDefault();
61
+ setError(null);
62
+
63
+ const values = Object.fromEntries(new FormData(event.currentTarget));
64
+ const schema = mode === "sign-in" ? signInSchema : signUpSchema;
65
+ const parsed = schema.safeParse(values);
66
+ if (!parsed.success) {
67
+ setError(parsed.error.issues[0]?.message ?? "Check the form");
68
+ return;
69
+ }
70
+
71
+ setPending(true);
72
+ const client = authClient();
73
+ const { error } =
74
+ mode === "sign-in"
75
+ ? await client.signIn.email(signInSchema.parse(values))
76
+ : await client.signUp.email(signUpSchema.parse(values));
77
+ setPending(false);
78
+
79
+ if (error) {
80
+ setError(error.message ?? "Something went wrong, please try again");
81
+ return;
82
+ }
83
+ navigate("/dashboard");
84
+ }
85
+
86
+ function switchTo(next: Mode) {
87
+ setMode(next);
88
+ setError(null);
89
+ }
90
+
91
+ return (
92
+ <div className="flex flex-col gap-6 rounded-xl border border-line bg-surface-raised p-6 shadow-sm">
93
+ <div className="grid grid-cols-2 gap-1 rounded-lg bg-surface p-1 text-sm">
94
+ {(["sign-in", "sign-up"] as const).map((m) => (
95
+ <button
96
+ key={m}
97
+ type="button"
98
+ onClick={() => switchTo(m)}
99
+ className={cn(
100
+ "rounded-md px-3 py-1.5 font-medium transition",
101
+ mode === m
102
+ ? "bg-surface-raised text-ink shadow-sm"
103
+ : "text-ink-muted hover:text-ink",
104
+ )}
105
+ >
106
+ {m === "sign-in" ? "Sign in" : "Create account"}
107
+ </button>
108
+ ))}
109
+ </div>
110
+
111
+ <form onSubmit={onSubmit} noValidate className="flex flex-col gap-4">
112
+ {mode === "sign-up" && (
113
+ <Field label="Name" name="name" autoComplete="name" />
114
+ )}
115
+ <Field label="Email" name="email" type="email" autoComplete="email" />
116
+ <Field
117
+ label="Password"
118
+ name="password"
119
+ type="password"
120
+ autoComplete={
121
+ mode === "sign-in" ? "current-password" : "new-password"
122
+ }
123
+ />
124
+
125
+ {error && (
126
+ <p role="alert" className="text-sm text-danger">
127
+ {error}
128
+ </p>
129
+ )}
130
+
131
+ <button
132
+ type="submit"
133
+ disabled={pending}
134
+ className="rounded-lg bg-accent px-4 py-2.5 font-medium text-white transition hover:bg-accent-strong disabled:opacity-60"
135
+ >
136
+ {pending
137
+ ? "Please wait…"
138
+ : mode === "sign-in"
139
+ ? "Sign in"
140
+ : "Create account"}
141
+ </button>
142
+ </form>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ function Field({
148
+ label,
149
+ ...props
150
+ }: { label: string } & React.InputHTMLAttributes<HTMLInputElement>) {
151
+ return (
152
+ <label className="flex flex-col gap-1.5 text-sm font-medium">
153
+ {label}
154
+ <input
155
+ required
156
+ {...props}
157
+ className="rounded-lg border border-line bg-surface-raised px-3 py-2 font-normal outline-none transition focus:border-accent focus:ring-2 focus:ring-accent/20"
158
+ />
159
+ </label>
160
+ );
161
+ }
@@ -0,0 +1,6 @@
1
+ import { index, type RouteConfig, route } from "@react-router/dev/routes";
2
+
3
+ export default [
4
+ index("routes/home.tsx"),
5
+ route("dashboard", "routes/dashboard.tsx"),
6
+ ] satisfies RouteConfig;
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@repo/web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "react-router build",
8
+ "dev": "dotenv-run -- react-router dev",
9
+ "start": "dotenv-run -- react-router-serve ./dist/server/index.js",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "typecheck": "react-router typegen && tsc"
13
+ },
14
+ "dependencies": {
15
+ "@react-router/node": "^8.5.0",
16
+ "@react-router/serve": "^8.5.0",
17
+ "@repo/config": "workspace:*",
18
+ "@repo/shared": "workspace:*",
19
+ "better-auth": "catalog:",
20
+ "clsx": "^2.1.1",
21
+ "hono": "catalog:",
22
+ "isbot": "^5.2.2",
23
+ "react": "catalog:",
24
+ "react-dom": "catalog:",
25
+ "react-router": "^8.5.0",
26
+ "tailwind-merge": "^3.7.0"
27
+ },
28
+ "devDependencies": {
29
+ "@dotenv-run/cli": "catalog:",
30
+ "@react-router/dev": "^8.5.0",
31
+ "@repo/api": "workspace:*",
32
+ "@tailwindcss/vite": "^4.3.3",
33
+ "@types/node": "catalog:",
34
+ "@types/react": "catalog:",
35
+ "@types/react-dom": "catalog:",
36
+ "tailwindcss": "^4.3.3",
37
+ "typescript": "catalog:",
38
+ "vite": "^8.3.5",
39
+ "vitest": "catalog:"
40
+ }
41
+ }
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <title>Pyro</title>
3
+ <path fill="#e8590c" d="M16 2c1.5 5-3 7.5-3 12a3 3 0 0 0 6 0c0-1.2-.4-2.3-1-3.3 3.6 1.8 7 5.6 7 10.3a9 9 0 0 1-18 0c0-7.4 7.2-11 9-19Z"/>
4
+ <path fill="#ffd8a8" d="M16 20c1.6 1.4 3 3.2 3 5a3 3 0 0 1-6 0c0-1.8 1.4-3.6 3-5Z"/>
5
+ </svg>
@@ -0,0 +1,6 @@
1
+ import type { Config } from "@react-router/dev/config";
2
+
3
+ export default {
4
+ ssr: true,
5
+ buildDirectory: "dist",
6
+ } satisfies Config;
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "lib": ["es2024", "dom", "dom.iterable"],
6
+ "types": ["node", "vite/client"],
7
+ "jsx": "react-jsx",
8
+ "rootDirs": [".", "./.react-router/types"],
9
+ "paths": {
10
+ "~/*": ["./app/*"]
11
+ },
12
+ "esModuleInterop": true,
13
+ "verbatimModuleSyntax": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": [
17
+ "**/*",
18
+ "**/.server/**/*",
19
+ "**/.client/**/*",
20
+ ".react-router/types/**/*"
21
+ ],
22
+ "exclude": ["dist", "node_modules"]
23
+ }
@@ -0,0 +1,19 @@
1
+ import { reactRouter } from "@react-router/dev/vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { defineConfig } from "vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [tailwindcss(), reactRouter()],
7
+ resolve: {
8
+ tsconfigPaths: true,
9
+ },
10
+ server: {
11
+ port: 3000,
12
+ strictPort: true,
13
+ },
14
+ optimizeDeps: {
15
+ // Route modules load lazily, so without this Vite discovers their deps on
16
+ // the first page visit and force-reloads the page.
17
+ entries: ["./app/**/*.tsx"],
18
+ },
19
+ });
@@ -0,0 +1,13 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "~": fileURLToPath(new URL("./app", import.meta.url)),
8
+ },
9
+ },
10
+ test: {
11
+ include: ["app/**/*.test.{ts,tsx}"],
12
+ },
13
+ });
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.6.3/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": ["**", "!**/drizzle", "!**/pnpm-lock.yaml"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2
16
+ },
17
+ "linter": {
18
+ "enabled": true,
19
+ "rules": {
20
+ "preset": "recommended"
21
+ }
22
+ },
23
+ "css": {
24
+ "parser": {
25
+ "tailwindDirectives": true
26
+ }
27
+ },
28
+ "javascript": {
29
+ "formatter": {
30
+ "quoteStyle": "double"
31
+ }
32
+ },
33
+ "assist": {
34
+ "enabled": true,
35
+ "actions": {
36
+ "source": {
37
+ "organizeImports": {
38
+ "level": "on",
39
+ "options": {
40
+ "groups": [
41
+ ":URL:",
42
+ ":NODE:",
43
+ [":PACKAGE:", "!@repo/**"],
44
+ "@repo/**",
45
+ ":ALIAS:",
46
+ ":PATH:"
47
+ ]
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ name: my-pyro-app-dev
2
+
3
+ services:
4
+ postgres:
5
+ image: postgres:18-alpine
6
+ restart: unless-stopped
7
+ environment:
8
+ POSTGRES_USER: ${POSTGRES_USER:-app}
9
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
10
+ POSTGRES_DB: ${POSTGRES_DB:-app}
11
+ ports:
12
+ - "15432:5432"
13
+ volumes:
14
+ - postgres_dev_data:/var/lib/postgresql
15
+ healthcheck:
16
+ test:
17
+ [
18
+ "CMD-SHELL",
19
+ "pg_isready -U ${POSTGRES_USER:-app} -d ${POSTGRES_DB:-app}",
20
+ ]
21
+ interval: 5s
22
+ timeout: 5s
23
+ retries: 10
24
+
25
+ volumes:
26
+ postgres_dev_data: