Skip to content

Commit e6ba242

Browse files
committed
feat: favicon for all three apps + Prisma/SQLite in every scaffold
**Favicon** - scripts/generate-favicon.mjs renders the header-logo artwork (accent-orange linear gradient, rounded square, 1px inner highlight) at 512×512 via puppeteer and writes favicon.svg + favicon.png into website/public, docs/public, examples/blog/public. - Each root layout adds a <link rel="icon"> block (svg for capable browsers, png fallback, apple-touch-icon). - Removed the old examples/blog/public/favicon.ico so everything uses the same brand artwork. **Prisma/SQLite in every scaffold** - packages/cli/lib/create.js now emits prisma/schema.prisma (SQLite provider + example User model) and lib/prisma.ts (singleton) for full-stack, api, and saas templates. Previously only saas got Prisma — api and full-stack had no DB story. - package.json dependencies always include @prisma/client + prisma, and scripts expose `db:migrate`, `db:generate`, `db:studio`. `predev` runs `prisma generate`; `prestart` runs `prisma migrate deploy`. - .env.example gets a DATABASE_URL line if it didn't already. - .gitignore adds prisma/dev.db + dev.db-journal. - SaaS template's own prisma files still overwrite the base after this block runs — its auth-centric schema wins for saas. - packages/cli/templates/AGENTS.md adds a Database section explaining the Prisma workflow + scripts for scaffolded apps.
1 parent e9c7c73 commit e6ba242

13 files changed

Lines changed: 195 additions & 3 deletions

File tree

docs/app/layout.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export function generateMetadata(ctx: { url: string }) {
3939

4040
export default function RootLayout({ children }: { children: unknown }) {
4141
return html`
42+
<link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
43+
<link rel="icon" href="/public/favicon.png" type="image/png">
44+
<link rel="apple-touch-icon" href="/public/favicon.png">
4245
<script>
4346
(function(){
4447
try {

docs/public/favicon.png

104 KB
Loading

docs/public/favicon.svg

Lines changed: 10 additions & 0 deletions
Loading

examples/blog/app/layout.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export function generateMetadata(ctx: { url: string }) {
6060
*/
6161
export default function RootLayout({ children }: { children: unknown }) {
6262
return html`
63+
<link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
64+
<link rel="icon" href="/public/favicon.png" type="image/png">
65+
<link rel="apple-touch-icon" href="/public/favicon.png">
6366
<script>
6467
(function(){
6568
try {

examples/blog/public/favicon.ico

Whitespace-only changes.

examples/blog/public/favicon.png

104 KB
Loading

examples/blog/public/favicon.svg

Lines changed: 10 additions & 0 deletions
Loading

packages/cli/lib/create.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
4242
'modules',
4343
'lib',
4444
'public',
45+
'prisma',
4546
'test/unit',
4647
'test/e2e',
4748
];
@@ -55,26 +56,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
5556
type: 'module',
5657
private: true,
5758
scripts: {
59+
predev: 'prisma generate',
60+
prestart: 'prisma migrate deploy',
5861
dev: 'webjs dev',
5962
build: 'webjs build',
6063
start: 'webjs start',
6164
test: 'webjs test',
6265
'test:server': 'webjs test --server',
6366
'test:browser': 'webjs test --browser',
6467
check: 'webjs check',
68+
'db:migrate': 'prisma migrate dev',
69+
'db:generate': 'prisma generate',
70+
'db:studio': 'prisma studio',
6571
},
6672
dependencies: {
73+
'@prisma/client': '^6.0.0',
6774
'@webjskit/cli': 'latest',
6875
'@webjskit/core': 'latest',
6976
'@webjskit/server': 'latest',
70-
...(isSaas ? { '@prisma/client': '^6.0.0' } : {}),
7177
},
7278
devDependencies: {
7379
esbuild: '^0.28.0',
80+
prisma: '^6.0.0',
7481
'@web/test-runner': '^0.20.0',
7582
'@web/test-runner-playwright': '^0.11.0',
7683
'playwright': '^1.59.0',
77-
...(isSaas ? { prisma: '^6.0.0' } : {}),
7884
},
7985
}, null, 2) + '\n');
8086

@@ -140,6 +146,64 @@ export async function scaffoldApp(name, cwd, opts = {}) {
140146
const preCommitPath = join(appDir, '.hooks', 'pre-commit');
141147
if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
142148

149+
// --- Prisma schema + client singleton (all templates) ---
150+
151+
await writeFile(join(appDir, 'prisma', 'schema.prisma'), `generator client {
152+
provider = "prisma-client-js"
153+
}
154+
155+
datasource db {
156+
// Defaults to SQLite at ./prisma/dev.db. Switch to postgresql / mysql
157+
// by changing the provider + DATABASE_URL in .env.
158+
provider = "sqlite"
159+
url = env("DATABASE_URL")
160+
}
161+
162+
// Example model — feel free to delete or extend.
163+
model User {
164+
id Int @id @default(autoincrement())
165+
email String @unique
166+
name String?
167+
createdAt DateTime @default(now())
168+
}
169+
`);
170+
171+
await writeFile(join(appDir, 'lib', 'prisma.ts'), `/**
172+
* Prisma client singleton. The \`globalThis\` trick keeps a single
173+
* instance across dev-server module reloads, so we don't open a new
174+
* DB connection on every file change.
175+
*/
176+
import { PrismaClient } from '@prisma/client';
177+
178+
const g = globalThis as unknown as { __prisma?: PrismaClient };
179+
180+
export const prisma = g.__prisma ?? new PrismaClient();
181+
if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
182+
`);
183+
184+
// Env vars: append DATABASE_URL to the .env.example the template
185+
// already copied (if present). The scaffold's root .env.example
186+
// lists auth secrets etc.; we just add the DB line idempotently.
187+
const envExample = join(appDir, '.env.example');
188+
if (existsSync(envExample)) {
189+
const cur = await readFile(envExample, 'utf8');
190+
if (!cur.includes('DATABASE_URL')) {
191+
await writeFile(envExample, cur.replace(/\n?$/, '\n') + '\nDATABASE_URL=file:./prisma/dev.db\n');
192+
}
193+
} else {
194+
await writeFile(envExample, 'DATABASE_URL=file:./prisma/dev.db\n');
195+
}
196+
197+
// .gitignore the generated SQLite file.
198+
const gitignore = join(appDir, '.gitignore');
199+
const gitignoreExtra = '\n# SQLite dev database\nprisma/dev.db\nprisma/dev.db-journal\n';
200+
if (existsSync(gitignore)) {
201+
const cur = await readFile(gitignore, 'utf8');
202+
if (!cur.includes('prisma/dev.db')) await writeFile(gitignore, cur + gitignoreExtra);
203+
} else {
204+
await writeFile(gitignore, 'node_modules\n.webjs\n' + gitignoreExtra);
205+
}
206+
143207
// --- App files (template-specific) ---
144208

145209
if (isApi) {

packages/cli/templates/AGENTS.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,49 @@ modules/<feature>/
6060
components/*.ts feature-scoped components
6161
utils/*.ts feature-scoped helpers
6262
types.ts feature types
63-
lib/ cross-cutting infra (prisma, session, auth config)
63+
lib/
64+
prisma.ts PrismaClient singleton (import from here, never `new PrismaClient()`)
65+
... other cross-cutting infra (session, auth config, etc.)
66+
prisma/
67+
schema.prisma Prisma schema — SQLite by default, switch provider for Postgres/MySQL
68+
dev.db SQLite file (gitignored); run `npm run db:migrate` to create
69+
migrations/ generated migration SQL
6470
public/ static assets, served at /public/*
6571
test/unit/*.test.ts unit tests (node --test)
6672
test/browser/*.test.ts browser tests (web-test-runner)
6773
middleware.ts root middleware (optional, outermost)
6874
```
6975

76+
## Database (Prisma + SQLite by default)
77+
78+
Every scaffold includes a Prisma setup pointed at a local SQLite file.
79+
First-run workflow:
80+
81+
```sh
82+
cp .env.example .env # DATABASE_URL is pre-filled for SQLite
83+
npm run db:migrate # creates prisma/dev.db + migration
84+
npm run dev # webjs dev + prisma generate via predev
85+
```
86+
87+
Scripts:
88+
89+
- `npm run db:migrate``prisma migrate dev` (dev-time schema changes + migration + generate)
90+
- `npm run db:generate``prisma generate` (regenerate client only)
91+
- `npm run db:studio``prisma studio` (GUI)
92+
- `predev` hook auto-runs `prisma generate` before `npm run dev`
93+
- `prestart` hook runs `prisma migrate deploy` before `npm start` (idempotent in prod)
94+
95+
Always import the client from `lib/prisma.ts` (never `new PrismaClient()` directly —
96+
the singleton avoids opening a new connection on every dev-server reload):
97+
98+
```ts
99+
import { prisma } from '../../../lib/prisma.ts';
100+
const users = await prisma.user.findMany();
101+
```
102+
103+
To switch to Postgres or MySQL: change `provider` in `prisma/schema.prisma`
104+
and the `DATABASE_URL` in `.env`.
105+
70106
## Imports
71107

72108
```ts

scripts/generate-favicon.mjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Generates a single favicon.png (and matching SVG source) that mirrors
4+
* the brand logo used in each app's header: a small rounded square with
5+
* the accent-orange gradient + subtle inner highlight.
6+
*
7+
* Writes into website/public, docs/public, examples/blog/public.
8+
*
9+
* node scripts/generate-favicon.mjs
10+
*/
11+
import puppeteer from 'puppeteer-core';
12+
import { writeFile } from 'node:fs/promises';
13+
import { resolve, dirname } from 'node:path';
14+
import { fileURLToPath } from 'node:url';
15+
16+
const __dirname = dirname(fileURLToPath(import.meta.url));
17+
const root = resolve(__dirname, '..');
18+
19+
const APPS = [
20+
resolve(root, 'website/public'),
21+
resolve(root, 'docs/public'),
22+
resolve(root, 'examples/blog/public'),
23+
];
24+
25+
// SVG that matches the header logo: linear gradient, rounded corners,
26+
// inner highlight ring. 512×512 so it down-scales cleanly to any size.
27+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
28+
<defs>
29+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
30+
<stop offset="0%" stop-color="oklch(0.82 0.14 55)"/>
31+
<stop offset="100%" stop-color="oklch(0.58 0.13 45)"/>
32+
</linearGradient>
33+
</defs>
34+
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="url(#g)"/>
35+
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="none" stroke="oklch(1 0 0 / 0.15)" stroke-width="6"/>
36+
</svg>`;
37+
38+
const browser = await puppeteer.launch({
39+
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
40+
headless: true,
41+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
42+
});
43+
const page = await browser.newPage();
44+
await page.setViewport({ width: 512, height: 512, deviceScaleFactor: 1 });
45+
await page.setContent(`<!doctype html><html><body style="margin:0;background:transparent">${svg}</body></html>`, { waitUntil: 'load' });
46+
const png = await page.screenshot({ type: 'png', omitBackground: true });
47+
await browser.close();
48+
49+
for (const pub of APPS) {
50+
await writeFile(resolve(pub, 'favicon.svg'), svg);
51+
await writeFile(resolve(pub, 'favicon.png'), png);
52+
console.log('wrote', pub + '/favicon.{svg,png}', `(png: ${Math.round(png.length / 1024)} kB)`);
53+
}

0 commit comments

Comments
 (0)