@getstrata/starter 0.1.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 (35) hide show
  1. package/README.md +21 -0
  2. package/dist/cli.js +77 -0
  3. package/dist/templates/.env.example +3 -0
  4. package/dist/templates/docker-compose.yml +14 -0
  5. package/dist/templates/package.json +23 -0
  6. package/dist/templates/public/assets/site.css +29 -0
  7. package/dist/templates/src/bootstrap/config.ts +18 -0
  8. package/dist/templates/src/bootstrap/database.ts +28 -0
  9. package/dist/templates/src/bootstrap/preload.ts +17 -0
  10. package/dist/templates/src/bootstrap/server.ts +35 -0
  11. package/dist/templates/src/db/fresh.ts +19 -0
  12. package/dist/templates/src/db/migrate.ts +35 -0
  13. package/dist/templates/src/lib/router.ts +60 -0
  14. package/dist/templates/src/lib/view.ts +24 -0
  15. package/dist/templates/src/routes.ts +23 -0
  16. package/dist/templates/templates/.env.example +3 -0
  17. package/dist/templates/templates/docker-compose.yml +14 -0
  18. package/dist/templates/templates/package.json +23 -0
  19. package/dist/templates/templates/public/assets/site.css +29 -0
  20. package/dist/templates/templates/src/bootstrap/config.ts +18 -0
  21. package/dist/templates/templates/src/bootstrap/database.ts +28 -0
  22. package/dist/templates/templates/src/bootstrap/preload.ts +17 -0
  23. package/dist/templates/templates/src/bootstrap/server.ts +35 -0
  24. package/dist/templates/templates/src/db/fresh.ts +19 -0
  25. package/dist/templates/templates/src/db/migrate.ts +35 -0
  26. package/dist/templates/templates/src/lib/router.ts +60 -0
  27. package/dist/templates/templates/src/lib/view.ts +24 -0
  28. package/dist/templates/templates/src/routes.ts +23 -0
  29. package/dist/templates/templates/tsconfig.json +14 -0
  30. package/dist/templates/templates/views/home.eta +5 -0
  31. package/dist/templates/templates/views/layouts/app.eta +18 -0
  32. package/dist/templates/tsconfig.json +14 -0
  33. package/dist/templates/views/home.eta +5 -0
  34. package/dist/templates/views/layouts/app.eta +18 -0
  35. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # create-strata
2
+
3
+ Scaffold a new [Strata](https://github.com/EyK-26/strata) application.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx @getstrata/starter my-app
9
+ # or after install: bunx create-strata my-app
10
+ ```
11
+
12
+ ## What you get
13
+
14
+ - Bun + TypeScript app using `@getstrata/core` and `@getstrata/bootstrap`
15
+ - Postgres via Docker Compose
16
+ - Eta templates, simple router, health check
17
+ - `db:migrate` and `db:fresh` scripts
18
+
19
+ ## Publish
20
+
21
+ Released from the strata monorepo on npm as `@getstrata/starter`.
package/dist/cli.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // cli.ts
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ var PLACEHOLDER = /\{\{PROJECT_NAME\}\}/g;
8
+ function usage() {
9
+ console.log(`Usage: create-strata [project-name]
10
+
11
+ Scaffold a new Strata application with Bun, @getstrata/core, and @getstrata/bootstrap.
12
+
13
+ Examples:
14
+ bunx @getstrata/starter my-app
15
+ bunx create-strata my-app
16
+ `);
17
+ }
18
+ function parseArgs(argv) {
19
+ const positional = argv.filter((arg) => !arg.startsWith("-"));
20
+ if (argv.includes("-h") || argv.includes("--help")) {
21
+ usage();
22
+ process.exit(0);
23
+ }
24
+ const projectName = positional[0] ?? "strata-app";
25
+ if (!/^[a-z0-9][a-z0-9-_]*$/i.test(projectName)) {
26
+ console.error("Project name must contain only letters, numbers, hyphens, and underscores.");
27
+ process.exit(1);
28
+ }
29
+ return {
30
+ projectName,
31
+ targetDir: resolve(process.cwd(), projectName)
32
+ };
33
+ }
34
+ function copyTemplate(source, target, projectName) {
35
+ mkdirSync(target, { recursive: true });
36
+ for (const entry of readdirSync(source)) {
37
+ const from = join(source, entry);
38
+ const to = join(target, entry.replace(PLACEHOLDER, projectName));
39
+ const info = statSync(from);
40
+ if (info.isDirectory()) {
41
+ copyTemplate(from, to, projectName);
42
+ continue;
43
+ }
44
+ let contents = readFileSync(from, "utf8");
45
+ if (contents.includes("{{PROJECT_NAME}}")) {
46
+ contents = contents.replace(PLACEHOLDER, projectName);
47
+ }
48
+ writeFileSync(to, contents, { mode: info.mode & 511 });
49
+ }
50
+ }
51
+ function main() {
52
+ const options = parseArgs(process.argv.slice(2));
53
+ if (!options)
54
+ process.exit(1);
55
+ const templateDir = join(import.meta.dir, "templates");
56
+ if (!existsSync(templateDir)) {
57
+ console.error("Template directory missing. Reinstall create-strata.");
58
+ process.exit(1);
59
+ }
60
+ if (existsSync(options.targetDir)) {
61
+ console.error(`Directory already exists: ${options.targetDir}`);
62
+ process.exit(1);
63
+ }
64
+ copyTemplate(templateDir, options.targetDir, options.projectName);
65
+ console.log(`
66
+ Created Strata app in ${options.projectName}/
67
+ `);
68
+ console.log("Next steps:");
69
+ console.log(` cd ${options.projectName}`);
70
+ console.log(" cp .env.example .env");
71
+ console.log(" docker compose up -d");
72
+ console.log(" bun install");
73
+ console.log(" bun run db:migrate");
74
+ console.log(` bun run dev
75
+ `);
76
+ }
77
+ main();
@@ -0,0 +1,3 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
2
+ PORT=3000
3
+ APP_URL=http://localhost:3000
@@ -0,0 +1,14 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: postgres
6
+ POSTGRES_PASSWORD: postgres
7
+ POSTGRES_DB: {{PROJECT_NAME}}
8
+ ports:
9
+ - "5432:5432"
10
+ volumes:
11
+ - pgdata:/var/lib/postgresql/data
12
+
13
+ volumes:
14
+ pgdata:
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --preload ./src/bootstrap/preload.ts --watch src/bootstrap/server.ts",
8
+ "start": "bun --preload ./src/bootstrap/preload.ts src/bootstrap/server.ts",
9
+ "db:migrate": "bun --preload ./src/bootstrap/preload.ts src/db/migrate.ts",
10
+ "db:fresh": "bun --preload ./src/bootstrap/preload.ts src/db/fresh.ts",
11
+ "check": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@getstrata/bootstrap": "^0.1.0",
15
+ "@getstrata/core": "^0.3.0",
16
+ "eta": "^4.6.0",
17
+ "postgres": "3.4.7"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "typescript": "^5.9.2"
22
+ }
23
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,28 @@
1
+ import postgres from "postgres";
2
+
3
+ let sql: ReturnType<typeof postgres> | null = null;
4
+
5
+ export function getSql() {
6
+ if (!sql) {
7
+ const url = process.env.DATABASE_URL;
8
+ if (!url) throw new Error("DATABASE_URL is required");
9
+ sql = postgres(url, { max: 5 });
10
+ }
11
+ return sql;
12
+ }
13
+
14
+ export async function pingDatabase(): Promise<boolean> {
15
+ try {
16
+ await getSql()`SELECT 1`;
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export async function closeDatabase() {
24
+ if (sql) {
25
+ await sql.end();
26
+ sql = null;
27
+ }
28
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const envPath = join(import.meta.dir, "../../.env");
5
+ if (existsSync(envPath)) {
6
+ for (const line of readFileSync(envPath, "utf8").split("\n")) {
7
+ const trimmed = line.trim();
8
+ if (!trimmed || trimmed.startsWith("#")) continue;
9
+ const idx = trimmed.indexOf("=");
10
+ if (idx === -1) continue;
11
+ const key = trimmed.slice(0, idx);
12
+ const value = trimmed.slice(idx + 1);
13
+ if (!process.env[key]) process.env[key] = value;
14
+ }
15
+ }
16
+
17
+ process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}";
@@ -0,0 +1,35 @@
1
+ import { createWebServer } from "@getstrata/bootstrap";
2
+ import "./preload.ts";
3
+ import { migrate } from "../db/migrate.ts";
4
+ import { Router } from "../lib/router.ts";
5
+ import { registerRoutes } from "../routes.ts";
6
+ import { loadConfig } from "./config.ts";
7
+ import { closeDatabase, pingDatabase } from "./database.ts";
8
+
9
+ const config = loadConfig();
10
+
11
+ await migrate();
12
+
13
+ const router = new Router();
14
+ registerRoutes(router);
15
+
16
+ const server = createWebServer({
17
+ port: config.port,
18
+ publicDir: "./public",
19
+ handle: (request) => router.handle(request),
20
+ });
21
+
22
+ console.log(`${config.appUrl} (port ${server.port})`);
23
+
24
+ if (!(await pingDatabase())) {
25
+ console.warn("Warning: database ping failed.");
26
+ }
27
+
28
+ async function shutdown() {
29
+ await closeDatabase();
30
+ server.stop();
31
+ process.exit(0);
32
+ }
33
+
34
+ process.on("SIGINT", shutdown);
35
+ process.on("SIGTERM", shutdown);
@@ -0,0 +1,19 @@
1
+ import { getSql } from "../bootstrap/database.ts";
2
+ import { migrate, seed } from "./migrate.ts";
3
+
4
+ const tables = ["notes"];
5
+
6
+ export async function fresh() {
7
+ const sql = getSql();
8
+ for (const table of tables) {
9
+ await sql.unsafe(`DROP TABLE IF EXISTS ${table} CASCADE`);
10
+ }
11
+ await migrate();
12
+ await seed();
13
+ }
14
+
15
+ if (import.meta.main) {
16
+ await fresh();
17
+ console.log("Database reset, migrated, and seeded.");
18
+ process.exit(0);
19
+ }
@@ -0,0 +1,35 @@
1
+ import { getSql } from "../bootstrap/database.ts";
2
+
3
+ const migrations = [
4
+ `CREATE TABLE IF NOT EXISTS notes (
5
+ id SERIAL PRIMARY KEY,
6
+ body TEXT NOT NULL,
7
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8
+ )`,
9
+ ];
10
+
11
+ export async function migrate() {
12
+ const sql = getSql();
13
+ for (const statement of migrations) {
14
+ await sql.unsafe(statement);
15
+ }
16
+ }
17
+
18
+ export async function seed() {
19
+ const sql = getSql();
20
+ const [{ count }] = await sql<{ count: string }[]>`
21
+ SELECT COUNT(*)::text AS count FROM notes
22
+ `;
23
+ if (Number(count) > 0) return;
24
+
25
+ await sql`
26
+ INSERT INTO notes (body) VALUES ('Welcome to Strata!')
27
+ `;
28
+ }
29
+
30
+ if (import.meta.main) {
31
+ await migrate();
32
+ await seed();
33
+ console.log("Database migrated and seeded.");
34
+ process.exit(0);
35
+ }
@@ -0,0 +1,60 @@
1
+ export type HttpMethod = "GET" | "POST";
2
+
3
+ export type RouteHandler = (
4
+ request: Request,
5
+ params: Record<string, string>,
6
+ ) => Response | Promise<Response>;
7
+
8
+ interface Route {
9
+ method: HttpMethod;
10
+ pattern: RegExp;
11
+ paramNames: string[];
12
+ handler: RouteHandler;
13
+ }
14
+
15
+ export class Router {
16
+ private routes: Route[] = [];
17
+
18
+ get(path: string, handler: RouteHandler) {
19
+ this.add("GET", path, handler);
20
+ }
21
+
22
+ post(path: string, handler: RouteHandler) {
23
+ this.add("POST", path, handler);
24
+ }
25
+
26
+ private add(method: HttpMethod, path: string, handler: RouteHandler) {
27
+ const paramNames: string[] = [];
28
+ const patternSource = path
29
+ .replace(/\//g, "\\/")
30
+ .replace(/:([a-zA-Z_]+)/g, (_, name: string) => {
31
+ paramNames.push(name);
32
+ return "([^/]+)";
33
+ });
34
+ this.routes.push({
35
+ method,
36
+ pattern: new RegExp(`^${patternSource}$`),
37
+ paramNames,
38
+ handler,
39
+ });
40
+ }
41
+
42
+ async handle(request: Request): Promise<Response | null> {
43
+ const url = new URL(request.url);
44
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
45
+
46
+ for (const route of this.routes) {
47
+ if (route.method !== request.method) continue;
48
+ const match = pathname.match(route.pattern);
49
+ if (!match) continue;
50
+
51
+ const params: Record<string, string> = {};
52
+ route.paramNames.forEach((name, index) => {
53
+ params[name] = decodeURIComponent(match[index + 1] ?? "");
54
+ });
55
+ return await route.handler(request, params);
56
+ }
57
+
58
+ return null;
59
+ }
60
+ }
@@ -0,0 +1,24 @@
1
+ import { join } from "node:path";
2
+ import { Eta } from "eta";
3
+
4
+ const eta = new Eta({ views: join(import.meta.dir, "../../views") });
5
+
6
+ export interface LayoutData {
7
+ title: string;
8
+ description?: string;
9
+ }
10
+
11
+ export async function renderPage(
12
+ template: string,
13
+ data: Record<string, unknown> & { layout: LayoutData },
14
+ ): Promise<Response> {
15
+ const body = await eta.renderAsync(template, data);
16
+ const html = await eta.renderAsync("layouts/app.eta", { ...data, body });
17
+ return new Response(html, {
18
+ headers: { "content-type": "text/html; charset=utf-8" },
19
+ });
20
+ }
21
+
22
+ export function plainText(body: string): Response {
23
+ return new Response(body, { headers: { "content-type": "text/plain; charset=utf-8" } });
24
+ }
@@ -0,0 +1,23 @@
1
+ import { withErrorHandling } from "@getstrata/core";
2
+ import { pingDatabase } from "./bootstrap/database.ts";
3
+ import type { Router } from "./lib/router.ts";
4
+ import { plainText, renderPage } from "./lib/view.ts";
5
+
6
+ export function registerRoutes(router: Router) {
7
+ router.get("/", async () =>
8
+ renderPage("home.eta", {
9
+ layout: {
10
+ title: "Home",
11
+ description: "A new Strata application",
12
+ },
13
+ }),
14
+ );
15
+
16
+ router.get(
17
+ "/health",
18
+ withErrorHandling(async () => {
19
+ const dbOk = await pingDatabase();
20
+ return plainText(dbOk ? "ok" : "degraded");
21
+ }),
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}
2
+ PORT=3000
3
+ APP_URL=http://localhost:3000
@@ -0,0 +1,14 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_USER: postgres
6
+ POSTGRES_PASSWORD: postgres
7
+ POSTGRES_DB: {{PROJECT_NAME}}
8
+ ports:
9
+ - "5432:5432"
10
+ volumes:
11
+ - pgdata:/var/lib/postgresql/data
12
+
13
+ volumes:
14
+ pgdata:
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --preload ./src/bootstrap/preload.ts --watch src/bootstrap/server.ts",
8
+ "start": "bun --preload ./src/bootstrap/preload.ts src/bootstrap/server.ts",
9
+ "db:migrate": "bun --preload ./src/bootstrap/preload.ts src/db/migrate.ts",
10
+ "db:fresh": "bun --preload ./src/bootstrap/preload.ts src/db/fresh.ts",
11
+ "check": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@getstrata/bootstrap": "^0.1.0",
15
+ "@getstrata/core": "^0.3.0",
16
+ "eta": "^4.6.0",
17
+ "postgres": "3.4.7"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "typescript": "^5.9.2"
22
+ }
23
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,18 @@
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
+ }
@@ -0,0 +1,28 @@
1
+ import postgres from "postgres";
2
+
3
+ let sql: ReturnType<typeof postgres> | null = null;
4
+
5
+ export function getSql() {
6
+ if (!sql) {
7
+ const url = process.env.DATABASE_URL;
8
+ if (!url) throw new Error("DATABASE_URL is required");
9
+ sql = postgres(url, { max: 5 });
10
+ }
11
+ return sql;
12
+ }
13
+
14
+ export async function pingDatabase(): Promise<boolean> {
15
+ try {
16
+ await getSql()`SELECT 1`;
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export async function closeDatabase() {
24
+ if (sql) {
25
+ await sql.end();
26
+ sql = null;
27
+ }
28
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const envPath = join(import.meta.dir, "../../.env");
5
+ if (existsSync(envPath)) {
6
+ for (const line of readFileSync(envPath, "utf8").split("\n")) {
7
+ const trimmed = line.trim();
8
+ if (!trimmed || trimmed.startsWith("#")) continue;
9
+ const idx = trimmed.indexOf("=");
10
+ if (idx === -1) continue;
11
+ const key = trimmed.slice(0, idx);
12
+ const value = trimmed.slice(idx + 1);
13
+ if (!process.env[key]) process.env[key] = value;
14
+ }
15
+ }
16
+
17
+ process.env.DATABASE_URL ??= "postgresql://postgres:postgres@localhost:5432/{{PROJECT_NAME}}";
@@ -0,0 +1,35 @@
1
+ import { createWebServer } from "@getstrata/bootstrap";
2
+ import "./preload.ts";
3
+ import { migrate } from "../db/migrate.ts";
4
+ import { Router } from "../lib/router.ts";
5
+ import { registerRoutes } from "../routes.ts";
6
+ import { loadConfig } from "./config.ts";
7
+ import { closeDatabase, pingDatabase } from "./database.ts";
8
+
9
+ const config = loadConfig();
10
+
11
+ await migrate();
12
+
13
+ const router = new Router();
14
+ registerRoutes(router);
15
+
16
+ const server = createWebServer({
17
+ port: config.port,
18
+ publicDir: "./public",
19
+ handle: (request) => router.handle(request),
20
+ });
21
+
22
+ console.log(`${config.appUrl} (port ${server.port})`);
23
+
24
+ if (!(await pingDatabase())) {
25
+ console.warn("Warning: database ping failed.");
26
+ }
27
+
28
+ async function shutdown() {
29
+ await closeDatabase();
30
+ server.stop();
31
+ process.exit(0);
32
+ }
33
+
34
+ process.on("SIGINT", shutdown);
35
+ process.on("SIGTERM", shutdown);
@@ -0,0 +1,19 @@
1
+ import { getSql } from "../bootstrap/database.ts";
2
+ import { migrate, seed } from "./migrate.ts";
3
+
4
+ const tables = ["notes"];
5
+
6
+ export async function fresh() {
7
+ const sql = getSql();
8
+ for (const table of tables) {
9
+ await sql.unsafe(`DROP TABLE IF EXISTS ${table} CASCADE`);
10
+ }
11
+ await migrate();
12
+ await seed();
13
+ }
14
+
15
+ if (import.meta.main) {
16
+ await fresh();
17
+ console.log("Database reset, migrated, and seeded.");
18
+ process.exit(0);
19
+ }
@@ -0,0 +1,35 @@
1
+ import { getSql } from "../bootstrap/database.ts";
2
+
3
+ const migrations = [
4
+ `CREATE TABLE IF NOT EXISTS notes (
5
+ id SERIAL PRIMARY KEY,
6
+ body TEXT NOT NULL,
7
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8
+ )`,
9
+ ];
10
+
11
+ export async function migrate() {
12
+ const sql = getSql();
13
+ for (const statement of migrations) {
14
+ await sql.unsafe(statement);
15
+ }
16
+ }
17
+
18
+ export async function seed() {
19
+ const sql = getSql();
20
+ const [{ count }] = await sql<{ count: string }[]>`
21
+ SELECT COUNT(*)::text AS count FROM notes
22
+ `;
23
+ if (Number(count) > 0) return;
24
+
25
+ await sql`
26
+ INSERT INTO notes (body) VALUES ('Welcome to Strata!')
27
+ `;
28
+ }
29
+
30
+ if (import.meta.main) {
31
+ await migrate();
32
+ await seed();
33
+ console.log("Database migrated and seeded.");
34
+ process.exit(0);
35
+ }
@@ -0,0 +1,60 @@
1
+ export type HttpMethod = "GET" | "POST";
2
+
3
+ export type RouteHandler = (
4
+ request: Request,
5
+ params: Record<string, string>,
6
+ ) => Response | Promise<Response>;
7
+
8
+ interface Route {
9
+ method: HttpMethod;
10
+ pattern: RegExp;
11
+ paramNames: string[];
12
+ handler: RouteHandler;
13
+ }
14
+
15
+ export class Router {
16
+ private routes: Route[] = [];
17
+
18
+ get(path: string, handler: RouteHandler) {
19
+ this.add("GET", path, handler);
20
+ }
21
+
22
+ post(path: string, handler: RouteHandler) {
23
+ this.add("POST", path, handler);
24
+ }
25
+
26
+ private add(method: HttpMethod, path: string, handler: RouteHandler) {
27
+ const paramNames: string[] = [];
28
+ const patternSource = path
29
+ .replace(/\//g, "\\/")
30
+ .replace(/:([a-zA-Z_]+)/g, (_, name: string) => {
31
+ paramNames.push(name);
32
+ return "([^/]+)";
33
+ });
34
+ this.routes.push({
35
+ method,
36
+ pattern: new RegExp(`^${patternSource}$`),
37
+ paramNames,
38
+ handler,
39
+ });
40
+ }
41
+
42
+ async handle(request: Request): Promise<Response | null> {
43
+ const url = new URL(request.url);
44
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
45
+
46
+ for (const route of this.routes) {
47
+ if (route.method !== request.method) continue;
48
+ const match = pathname.match(route.pattern);
49
+ if (!match) continue;
50
+
51
+ const params: Record<string, string> = {};
52
+ route.paramNames.forEach((name, index) => {
53
+ params[name] = decodeURIComponent(match[index + 1] ?? "");
54
+ });
55
+ return await route.handler(request, params);
56
+ }
57
+
58
+ return null;
59
+ }
60
+ }
@@ -0,0 +1,24 @@
1
+ import { join } from "node:path";
2
+ import { Eta } from "eta";
3
+
4
+ const eta = new Eta({ views: join(import.meta.dir, "../../views") });
5
+
6
+ export interface LayoutData {
7
+ title: string;
8
+ description?: string;
9
+ }
10
+
11
+ export async function renderPage(
12
+ template: string,
13
+ data: Record<string, unknown> & { layout: LayoutData },
14
+ ): Promise<Response> {
15
+ const body = await eta.renderAsync(template, data);
16
+ const html = await eta.renderAsync("layouts/app.eta", { ...data, body });
17
+ return new Response(html, {
18
+ headers: { "content-type": "text/html; charset=utf-8" },
19
+ });
20
+ }
21
+
22
+ export function plainText(body: string): Response {
23
+ return new Response(body, { headers: { "content-type": "text/plain; charset=utf-8" } });
24
+ }
@@ -0,0 +1,23 @@
1
+ import { withErrorHandling } from "@getstrata/core";
2
+ import { pingDatabase } from "./bootstrap/database.ts";
3
+ import type { Router } from "./lib/router.ts";
4
+ import { plainText, renderPage } from "./lib/view.ts";
5
+
6
+ export function registerRoutes(router: Router) {
7
+ router.get("/", async () =>
8
+ renderPage("home.eta", {
9
+ layout: {
10
+ title: "Home",
11
+ description: "A new Strata application",
12
+ },
13
+ }),
14
+ );
15
+
16
+ router.get(
17
+ "/health",
18
+ withErrorHandling(async () => {
19
+ const dbOk = await pingDatabase();
20
+ return plainText(dbOk ? "ok" : "degraded");
21
+ }),
22
+ );
23
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "types": ["bun"],
11
+ "lib": ["ES2022", "DOM"]
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,5 @@
1
+ <section class="section">
2
+ <h1>Welcome to Strata</h1>
3
+ <p>Your app <strong>{{PROJECT_NAME}}</strong> is running.</p>
4
+ <p>Edit <code>src/routes.ts</code> and <code>views/home.eta</code> to get started.</p>
5
+ </section>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title><%= it.layout.title %> · Strata</title>
7
+ <% if (it.layout.description) { %>
8
+ <meta name="description" content="<%= it.layout.description %>" />
9
+ <% } %>
10
+ <link rel="stylesheet" href="/assets/site.css" />
11
+ </head>
12
+ <body>
13
+ <header class="site-header">
14
+ <a class="brand" href="/">Strata</a>
15
+ </header>
16
+ <main><%~ it.body %></main>
17
+ </body>
18
+ </html>
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "types": ["bun"],
11
+ "lib": ["ES2022", "DOM"]
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,5 @@
1
+ <section class="section">
2
+ <h1>Welcome to Strata</h1>
3
+ <p>Your app <strong>{{PROJECT_NAME}}</strong> is running.</p>
4
+ <p>Edit <code>src/routes.ts</code> and <code>views/home.eta</code> to get started.</p>
5
+ </section>
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title><%= it.layout.title %> · Strata</title>
7
+ <% if (it.layout.description) { %>
8
+ <meta name="description" content="<%= it.layout.description %>" />
9
+ <% } %>
10
+ <link rel="stylesheet" href="/assets/site.css" />
11
+ </head>
12
+ <body>
13
+ <header class="site-header">
14
+ <a class="brand" href="/">Strata</a>
15
+ </header>
16
+ <main><%~ it.body %></main>
17
+ </body>
18
+ </html>
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@getstrata/starter",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Strata app — bun create strata",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/EyK-26/strata.git",
10
+ "directory": "packages/strata-starter"
11
+ },
12
+ "bin": {
13
+ "create-strata": "./dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun build cli.ts --outdir dist --target bun && cp -r templates dist/templates",
21
+ "prepublishOnly": "bun run build"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "engines": {
27
+ "bun": ">=1.1.0"
28
+ }
29
+ }