@getstrata/starter 0.1.8 → 0.1.9
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.
- package/README.md +9 -22
- package/dist/cli.js +2400 -47
- package/dist/templates/overlays/api/docs/API.md +9 -0
- package/dist/templates/overlays/server-htmx/public/assets/app.css +89 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/error.eta +14 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/forbidden.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/errors/not-found.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/layouts/app.eta +34 -0
- package/dist/templates/overlays/server-htmx/resources/views/organizations/_table.eta +23 -0
- package/dist/templates/overlays/server-htmx/resources/views/organizations/index.eta +37 -0
- package/dist/templates/overlays/server-htmx/resources/views/pages/home.eta +4 -0
- package/dist/templates/overlays/server-htmx/resources/views/partials/_flash.eta +3 -0
- package/dist/templates/overlays/spa-react/frontend/build.ts +17 -0
- package/dist/templates/overlays/spa-react/frontend/bun-env.d.ts +4 -0
- package/dist/templates/overlays/spa-react/frontend/bun.lock +51 -0
- package/dist/templates/overlays/spa-react/frontend/dev-server.ts +66 -0
- package/dist/templates/overlays/spa-react/frontend/index.html +12 -0
- package/dist/templates/overlays/spa-react/frontend/package.json +21 -0
- package/dist/templates/overlays/spa-react/frontend/src/App.tsx +80 -0
- package/dist/templates/overlays/spa-react/frontend/src/api/client.ts +86 -0
- package/dist/templates/overlays/spa-react/frontend/src/app.css +98 -0
- package/dist/templates/overlays/spa-react/frontend/src/auth/AuthContext.tsx +103 -0
- package/dist/templates/overlays/spa-react/frontend/src/auth/tokenStorage.ts +15 -0
- package/dist/templates/overlays/spa-react/frontend/src/main.tsx +22 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/LoginPage.tsx +67 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/NotFoundPage.tsx +12 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/OrganizationsPage.tsx +69 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/ProjectsPage.tsx +65 -0
- package/dist/templates/overlays/spa-react/frontend/src/pages/TasksPage.tsx +65 -0
- package/dist/templates/{templates → overlays/spa-react/frontend}/tsconfig.json +9 -6
- package/dist/templates/package.json +1 -1
- package/package.json +3 -3
- package/dist/templates/templates/.env.example +0 -22
- package/dist/templates/templates/docker-compose.yml +0 -14
- package/dist/templates/templates/package.json +0 -22
- package/dist/templates/templates/public/assets/site.css +0 -29
- package/dist/templates/templates/src/bootstrap/config.ts +0 -18
- package/dist/templates/templates/src/bootstrap/createApp.ts +0 -93
- package/dist/templates/templates/src/bootstrap/database.ts +0 -30
- package/dist/templates/templates/src/bootstrap/preload.ts +0 -5
- package/dist/templates/templates/src/bootstrap/providers/auth.ts +0 -41
- package/dist/templates/templates/src/bootstrap/providers/cache.ts +0 -28
- package/dist/templates/templates/src/bootstrap/providers/config.ts +0 -27
- package/dist/templates/templates/src/bootstrap/providers/index.ts +0 -14
- package/dist/templates/templates/src/bootstrap/providers/storage.ts +0 -11
- package/dist/templates/templates/src/bootstrap/server.ts +0 -25
- package/dist/templates/templates/src/db/fresh.ts +0 -19
- package/dist/templates/templates/src/db/migrate.ts +0 -35
- package/dist/templates/templates/src/lib/view.ts +0 -21
- package/dist/templates/templates/src/modules/site/index.ts +0 -29
- package/dist/templates/templates/src/routes.ts +0 -6
- package/dist/templates/templates/strata.config.ts +0 -7
- package/dist/templates/templates/views/home.eta +0 -5
- package/dist/templates/templates/views/layouts/app.eta +0 -18
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { apiFetch } from "../api/client";
|
|
11
|
+
import { clearToken, readToken, writeToken } from "./tokenStorage";
|
|
12
|
+
|
|
13
|
+
interface AuthUser {
|
|
14
|
+
id: number;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
role: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AuthContextValue {
|
|
21
|
+
token: string | null;
|
|
22
|
+
user: AuthUser | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
login: (email: string, password: string) => Promise<void>;
|
|
25
|
+
logout: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
function AuthProvider({ children }: { children: ReactNode }) {
|
|
31
|
+
const [token, setToken] = useState<string | null>(() => readToken());
|
|
32
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
33
|
+
const [loading, setLoading] = useState(Boolean(readToken()));
|
|
34
|
+
|
|
35
|
+
const loadProfile = useCallback(async (activeToken: string) => {
|
|
36
|
+
const profile = await apiFetch<AuthUser>("/auth/me", { token: activeToken });
|
|
37
|
+
setUser(profile);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!token) {
|
|
42
|
+
setUser(null);
|
|
43
|
+
setLoading(false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadProfile(token)
|
|
48
|
+
.catch(() => {
|
|
49
|
+
clearToken();
|
|
50
|
+
setToken(null);
|
|
51
|
+
setUser(null);
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
});
|
|
56
|
+
}, [loadProfile, token]);
|
|
57
|
+
|
|
58
|
+
const login = useCallback(
|
|
59
|
+
async (email: string, password: string) => {
|
|
60
|
+
const body = await apiFetch<{ token: string }>("/auth/login", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: JSON.stringify({ email, password }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
writeToken(body.token);
|
|
66
|
+
setToken(body.token);
|
|
67
|
+
await loadProfile(body.token);
|
|
68
|
+
},
|
|
69
|
+
[loadProfile],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const logout = useCallback(() => {
|
|
73
|
+
clearToken();
|
|
74
|
+
setToken(null);
|
|
75
|
+
setUser(null);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const value = useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
token,
|
|
81
|
+
user,
|
|
82
|
+
loading,
|
|
83
|
+
login,
|
|
84
|
+
logout,
|
|
85
|
+
}),
|
|
86
|
+
[loading, login, logout, token, user],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useAuth(): AuthContextValue {
|
|
93
|
+
const context = useContext(AuthContext);
|
|
94
|
+
|
|
95
|
+
if (!context) {
|
|
96
|
+
throw new Error("useAuth must be used within AuthProvider");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return context;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type { AuthUser };
|
|
103
|
+
export { AuthProvider, useAuth };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const TOKEN_KEY = "strata_api_token";
|
|
2
|
+
|
|
3
|
+
function readToken(): string | null {
|
|
4
|
+
return localStorage.getItem(TOKEN_KEY);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function writeToken(token: string): void {
|
|
8
|
+
localStorage.setItem(TOKEN_KEY, token);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function clearToken(): void {
|
|
12
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { clearToken, readToken, TOKEN_KEY, writeToken };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
import App from "./App";
|
|
5
|
+
import { AuthProvider } from "./auth/AuthContext";
|
|
6
|
+
import "./app.css";
|
|
7
|
+
|
|
8
|
+
const rootElement = document.getElementById("root");
|
|
9
|
+
|
|
10
|
+
if (!rootElement) {
|
|
11
|
+
throw new Error("Root element #root not found");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createRoot(rootElement).render(
|
|
15
|
+
<StrictMode>
|
|
16
|
+
<BrowserRouter basename="/app">
|
|
17
|
+
<AuthProvider>
|
|
18
|
+
<App />
|
|
19
|
+
</AuthProvider>
|
|
20
|
+
</BrowserRouter>
|
|
21
|
+
</StrictMode>,
|
|
22
|
+
);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type FormEvent, useState } from "react";
|
|
2
|
+
import { Navigate, useNavigate } from "react-router-dom";
|
|
3
|
+
import { ApiError } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
export default function LoginPage() {
|
|
7
|
+
const { login, token } = useAuth();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const [email, setEmail] = useState("admin@example.test");
|
|
10
|
+
const [password, setPassword] = useState("password");
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
const [submitting, setSubmitting] = useState(false);
|
|
13
|
+
|
|
14
|
+
if (token) {
|
|
15
|
+
return <Navigate to="/organizations" replace />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function onSubmit(event: FormEvent<HTMLFormElement>) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
setSubmitting(true);
|
|
21
|
+
setError(null);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await login(email, password);
|
|
25
|
+
navigate("/organizations");
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
setError(cause instanceof ApiError ? cause.message : "Login failed");
|
|
28
|
+
} finally {
|
|
29
|
+
setSubmitting(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<section className="card">
|
|
35
|
+
<h1>Sign in</h1>
|
|
36
|
+
<p className="hint">Use seeded credentials or your own API user.</p>
|
|
37
|
+
|
|
38
|
+
{error ? <p className="error">{error}</p> : null}
|
|
39
|
+
|
|
40
|
+
<form className="stack-form" onSubmit={onSubmit}>
|
|
41
|
+
<label>
|
|
42
|
+
Email
|
|
43
|
+
<input
|
|
44
|
+
type="email"
|
|
45
|
+
value={email}
|
|
46
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</label>
|
|
50
|
+
|
|
51
|
+
<label>
|
|
52
|
+
Password
|
|
53
|
+
<input
|
|
54
|
+
type="password"
|
|
55
|
+
value={password}
|
|
56
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
57
|
+
required
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
|
|
61
|
+
<button type="submit" disabled={submitting}>
|
|
62
|
+
{submitting ? "Signing in…" : "Sign in"}
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
</section>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Organization {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PaginatedOrganizations {
|
|
13
|
+
data: Organization[];
|
|
14
|
+
meta: {
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function OrganizationsPage() {
|
|
20
|
+
const { token } = useAuth();
|
|
21
|
+
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!token) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
apiFetch<PaginatedOrganizations>("/organizations", { token })
|
|
30
|
+
.then((body) => setOrganizations(body.data))
|
|
31
|
+
.catch((cause: unknown) => {
|
|
32
|
+
setError(cause instanceof Error ? cause.message : "Failed to load organizations");
|
|
33
|
+
});
|
|
34
|
+
}, [token]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<section>
|
|
38
|
+
<h1>Organizations</h1>
|
|
39
|
+
<p className="hint">{organizations.length} loaded from the JSON API.</p>
|
|
40
|
+
|
|
41
|
+
{error ? <p className="error">{error}</p> : null}
|
|
42
|
+
|
|
43
|
+
<div className="card">
|
|
44
|
+
<table>
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th>Name</th>
|
|
48
|
+
<th>Slug</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{organizations.map((organization) => (
|
|
53
|
+
<tr key={organization.id}>
|
|
54
|
+
<td>{organization.name}</td>
|
|
55
|
+
<td>
|
|
56
|
+
<code>{organization.slug}</code>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
))}
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<p>
|
|
65
|
+
Continue to <Link to="/projects">projects</Link> or <Link to="/tasks">tasks</Link>.
|
|
66
|
+
</p>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Project {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
status: string;
|
|
10
|
+
organization_id: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PaginatedProjects {
|
|
14
|
+
data: Project[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ProjectsPage() {
|
|
18
|
+
const { token } = useAuth();
|
|
19
|
+
const [projects, setProjects] = useState<Project[]>([]);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!token) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
apiFetch<PaginatedProjects>("/projects?include=organization", { token })
|
|
28
|
+
.then((body) => setProjects(body.data))
|
|
29
|
+
.catch((cause: unknown) => {
|
|
30
|
+
setError(cause instanceof Error ? cause.message : "Failed to load projects");
|
|
31
|
+
});
|
|
32
|
+
}, [token]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section>
|
|
36
|
+
<h1>Projects</h1>
|
|
37
|
+
{error ? <p className="error">{error}</p> : null}
|
|
38
|
+
|
|
39
|
+
<div className="card">
|
|
40
|
+
<table>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th>Name</th>
|
|
44
|
+
<th>Status</th>
|
|
45
|
+
<th>Organization</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
{projects.map((project) => (
|
|
50
|
+
<tr key={project.id}>
|
|
51
|
+
<td>{project.name}</td>
|
|
52
|
+
<td>{project.status}</td>
|
|
53
|
+
<td>{project.organization_id}</td>
|
|
54
|
+
</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p>
|
|
61
|
+
Back to <Link to="/organizations">organizations</Link>.
|
|
62
|
+
</p>
|
|
63
|
+
</section>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { apiFetch } from "../api/client";
|
|
4
|
+
import { useAuth } from "../auth/AuthContext";
|
|
5
|
+
|
|
6
|
+
interface Task {
|
|
7
|
+
id: number;
|
|
8
|
+
title: string;
|
|
9
|
+
status: string;
|
|
10
|
+
project_id: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PaginatedTasks {
|
|
14
|
+
data: Task[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function TasksPage() {
|
|
18
|
+
const { token } = useAuth();
|
|
19
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!token) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
apiFetch<PaginatedTasks>("/tasks", { token })
|
|
28
|
+
.then((body) => setTasks(body.data))
|
|
29
|
+
.catch((cause: unknown) => {
|
|
30
|
+
setError(cause instanceof Error ? cause.message : "Failed to load tasks");
|
|
31
|
+
});
|
|
32
|
+
}, [token]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<section>
|
|
36
|
+
<h1>Tasks</h1>
|
|
37
|
+
{error ? <p className="error">{error}</p> : null}
|
|
38
|
+
|
|
39
|
+
<div className="card">
|
|
40
|
+
<table>
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th>Title</th>
|
|
44
|
+
<th>Status</th>
|
|
45
|
+
<th>Project</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
<tbody>
|
|
49
|
+
{tasks.map((task) => (
|
|
50
|
+
<tr key={task.id}>
|
|
51
|
+
<td>{task.title}</td>
|
|
52
|
+
<td>{task.status}</td>
|
|
53
|
+
<td>{task.project_id}</td>
|
|
54
|
+
</tr>
|
|
55
|
+
))}
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p>
|
|
61
|
+
Back to <Link to="/organizations">organizations</Link>.
|
|
62
|
+
</p>
|
|
63
|
+
</section>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
4
5
|
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
6
|
"skipLibCheck": true,
|
|
8
|
-
"
|
|
7
|
+
"moduleResolution": "bundler",
|
|
9
8
|
"allowImportingTsExtensions": true,
|
|
10
|
-
"
|
|
11
|
-
"
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"types": ["bun"]
|
|
12
15
|
},
|
|
13
|
-
"include": ["src
|
|
16
|
+
"include": ["src", "build.ts", "dev-server.ts", "bun-env.d.ts"]
|
|
14
17
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getstrata/starter",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Interactive create-strata wizard. Choose each layer; one database engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "bun build cli.ts --outdir dist --target bun && cp -r templates dist/templates",
|
|
20
|
+
"build": "bun build cli.ts --outdir dist --target bun && rm -rf dist/templates && mkdir -p dist/templates/overlays && cp -r templates/. dist/templates && cp -r ../../templates/scaffold/. dist/templates/overlays",
|
|
21
21
|
"prepublishOnly": "bun run build"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
|
|
2
|
-
PORT=3000
|
|
3
|
-
APP_URL=http://localhost:3000
|
|
4
|
-
APP_ENV=local
|
|
5
|
-
APP_NAME={{PROJECT_NAME}}
|
|
6
|
-
# Cookie/Redis namespace. Unset defaults to `strata`.
|
|
7
|
-
# APP_KEY_PREFIX={{PROJECT_NAME}}
|
|
8
|
-
FRONTEND_MODE=api
|
|
9
|
-
# SPA_PREFIX=/app
|
|
10
|
-
TENANCY_DRIVER=none
|
|
11
|
-
|
|
12
|
-
# Required in production when FRONTEND_MODE=server-htmx or hybrid
|
|
13
|
-
# SESSION_SECRET=
|
|
14
|
-
|
|
15
|
-
# Set only behind a trusted reverse proxy
|
|
16
|
-
# TRUST_FORWARDED_FOR=true
|
|
17
|
-
|
|
18
|
-
# Required in production to expose GET /metrics
|
|
19
|
-
# METRICS_TOKEN=
|
|
20
|
-
|
|
21
|
-
# Production should set FEATURE_PUBLIC_READS=false
|
|
22
|
-
# FEATURE_PUBLIC_READS=false
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{PROJECT_NAME}}",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "strata dev",
|
|
8
|
-
"start": "strata start",
|
|
9
|
-
"db:migrate": "strata migrate",
|
|
10
|
-
"db:fresh": "strata migrate:fresh",
|
|
11
|
-
"check": "tsc --noEmit"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@getstrata/bootstrap": "^0.4.2",
|
|
15
|
-
"@getstrata/cli": "^0.2.0",
|
|
16
|
-
"@getstrata/core": "^0.7.2"
|
|
17
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@types/bun": "^1.4.0",
|
|
20
|
-
"typescript": "^5.9.2"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color-scheme: light dark;
|
|
3
|
-
font-family: system-ui, sans-serif;
|
|
4
|
-
line-height: 1.5;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
margin: 0;
|
|
9
|
-
padding: 0 1.5rem 2rem;
|
|
10
|
-
max-width: 48rem;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.site-header {
|
|
14
|
-
padding: 1rem 0;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.brand {
|
|
18
|
-
font-weight: 700;
|
|
19
|
-
text-decoration: none;
|
|
20
|
-
color: inherit;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.section h1 {
|
|
24
|
-
margin-top: 0;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
code {
|
|
28
|
-
font-size: 0.9em;
|
|
29
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface AppConfig {
|
|
2
|
-
port: number;
|
|
3
|
-
appUrl: string;
|
|
4
|
-
databaseUrl: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function loadConfig(): AppConfig {
|
|
8
|
-
const databaseUrl = process.env.DATABASE_URL;
|
|
9
|
-
if (!databaseUrl) {
|
|
10
|
-
throw new Error("DATABASE_URL is required");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
port: Number(process.env.PORT ?? 3000),
|
|
15
|
-
appUrl: process.env.APP_URL ?? "http://localhost:3000",
|
|
16
|
-
databaseUrl,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { runProviderPhase } from "@getstrata/bootstrap/context";
|
|
2
|
-
import {
|
|
3
|
-
type AppContext,
|
|
4
|
-
type AppDependencies,
|
|
5
|
-
type AppRouteMap,
|
|
6
|
-
assertAppDependenciesComplete,
|
|
7
|
-
type ConfigStore,
|
|
8
|
-
type MutableAppDependencies,
|
|
9
|
-
type ProviderContext,
|
|
10
|
-
ServiceContainer,
|
|
11
|
-
} from "@getstrata/bootstrap/contracts";
|
|
12
|
-
import { createWebServer } from "@getstrata/bootstrap/web/server";
|
|
13
|
-
import { setActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
|
|
14
|
-
import { migrate } from "../db/migrate.ts";
|
|
15
|
-
import { buildRoutes } from "../routes.ts";
|
|
16
|
-
import { loadConfig } from "./config.ts";
|
|
17
|
-
import { starterProviders } from "./providers/index.ts";
|
|
18
|
-
|
|
19
|
-
export interface BootstrapOptions {
|
|
20
|
-
migrate?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface BootstrappedApp {
|
|
24
|
-
context: AppContext;
|
|
25
|
-
routes: AppRouteMap;
|
|
26
|
-
config: ReturnType<typeof loadConfig>;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class AppConfigStore {
|
|
30
|
-
private readonly values = new Map<string, unknown>();
|
|
31
|
-
|
|
32
|
-
set<T>(key: string, value: T): T {
|
|
33
|
-
this.values.set(key, value);
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get<T>(key: string): T | undefined {
|
|
38
|
-
return this.values.get(key) as T | undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
require<T>(key: string): T {
|
|
42
|
-
const value = this.get<T>(key);
|
|
43
|
-
if (value === undefined) {
|
|
44
|
-
throw new Error(`Missing required config value "${key}".`);
|
|
45
|
-
}
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
has(key: string): boolean {
|
|
50
|
-
return this.values.has(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function createAppContext(): AppContext {
|
|
55
|
-
const container = new ServiceContainer();
|
|
56
|
-
const config = new AppConfigStore() as unknown as ConfigStore;
|
|
57
|
-
const dependencies: MutableAppDependencies = { container };
|
|
58
|
-
const context: ProviderContext = { container, config, dependencies };
|
|
59
|
-
|
|
60
|
-
runProviderPhase(starterProviders, "register", context);
|
|
61
|
-
runProviderPhase(starterProviders, "boot", context);
|
|
62
|
-
|
|
63
|
-
assertAppDependenciesComplete(dependencies);
|
|
64
|
-
|
|
65
|
-
const appContext = { container, config, dependencies: dependencies as AppDependencies };
|
|
66
|
-
setActiveApplicationContext(appContext as never);
|
|
67
|
-
return appContext;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function bootstrapApp(options: BootstrapOptions = {}): Promise<BootstrappedApp> {
|
|
71
|
-
const { migrate: runMigrate = true } = options;
|
|
72
|
-
|
|
73
|
-
const appConfig = loadConfig();
|
|
74
|
-
const context = createAppContext();
|
|
75
|
-
|
|
76
|
-
if (runMigrate) {
|
|
77
|
-
await migrate();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const routes = buildRoutes(context.dependencies);
|
|
81
|
-
|
|
82
|
-
return { context, routes, config: appConfig };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function createAppServer(routes: AppRouteMap, port = 0) {
|
|
86
|
-
return createWebServer({
|
|
87
|
-
port,
|
|
88
|
-
publicDir: "./public",
|
|
89
|
-
routes,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export { createAppContext };
|