@webjskit/cli 0.1.0 → 0.1.1
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/lib/create.js +72 -14
- package/package.json +1 -1
- package/templates/AGENTS.md +195 -0
- package/templates/CONVENTIONS.md +7 -2
package/lib/create.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
42
42
|
'modules',
|
|
43
43
|
'lib',
|
|
44
44
|
'public',
|
|
45
|
+
'prisma',
|
|
45
46
|
'test/unit',
|
|
46
47
|
'test/e2e',
|
|
47
48
|
];
|
|
@@ -55,6 +56,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
55
56
|
type: 'module',
|
|
56
57
|
private: true,
|
|
57
58
|
scripts: {
|
|
59
|
+
predev: 'prisma generate',
|
|
60
|
+
prestart: 'prisma migrate deploy',
|
|
58
61
|
dev: 'webjs dev',
|
|
59
62
|
build: 'webjs build',
|
|
60
63
|
start: 'webjs start',
|
|
@@ -62,19 +65,22 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
62
65
|
'test:server': 'webjs test --server',
|
|
63
66
|
'test:browser': 'webjs test --browser',
|
|
64
67
|
check: 'webjs check',
|
|
68
|
+
'db:migrate': 'prisma migrate dev',
|
|
69
|
+
'db:generate': 'prisma generate',
|
|
70
|
+
'db:studio': 'prisma studio',
|
|
65
71
|
},
|
|
66
72
|
dependencies: {
|
|
73
|
+
'@prisma/client': '^6.0.0',
|
|
67
74
|
'@webjskit/cli': 'latest',
|
|
68
75
|
'@webjskit/core': 'latest',
|
|
69
76
|
'@webjskit/server': 'latest',
|
|
70
|
-
...(isSaas ? { '@prisma/client': '^6.0.0' } : {}),
|
|
71
77
|
},
|
|
72
78
|
devDependencies: {
|
|
73
79
|
esbuild: '^0.28.0',
|
|
80
|
+
prisma: '^6.0.0',
|
|
74
81
|
'@web/test-runner': '^0.20.0',
|
|
75
82
|
'@web/test-runner-playwright': '^0.11.0',
|
|
76
83
|
'playwright': '^1.59.0',
|
|
77
|
-
...(isSaas ? { prisma: '^6.0.0' } : {}),
|
|
78
84
|
},
|
|
79
85
|
}, null, 2) + '\n');
|
|
80
86
|
|
|
@@ -95,9 +101,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
95
101
|
},
|
|
96
102
|
}, null, 2) + '\n');
|
|
97
103
|
|
|
98
|
-
// --- Templates (CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
|
|
104
|
+
// --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
|
|
99
105
|
|
|
100
106
|
const templateFiles = [
|
|
107
|
+
'AGENTS.md',
|
|
101
108
|
'CONVENTIONS.md',
|
|
102
109
|
'CLAUDE.md',
|
|
103
110
|
'test/unit/example.test.ts',
|
|
@@ -139,6 +146,64 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
139
146
|
const preCommitPath = join(appDir, '.hooks', 'pre-commit');
|
|
140
147
|
if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
|
|
141
148
|
|
|
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
|
+
|
|
142
207
|
// --- App files (template-specific) ---
|
|
143
208
|
|
|
144
209
|
if (isApi) {
|
|
@@ -386,12 +451,8 @@ export default function Home() {
|
|
|
386
451
|
}
|
|
387
452
|
`);
|
|
388
453
|
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
const agentsSrc = resolve(__dirname, '..', '..', '..', 'AGENTS.md');
|
|
392
|
-
if (existsSync(agentsSrc)) {
|
|
393
|
-
await cp(agentsSrc, join(appDir, 'AGENTS.md'));
|
|
394
|
-
}
|
|
454
|
+
// AGENTS.md is copied via the `templateFiles` loop above, from
|
|
455
|
+
// `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
|
|
395
456
|
|
|
396
457
|
// --- Theme toggle component ---
|
|
397
458
|
|
|
@@ -459,11 +520,8 @@ ThemeToggle.register('theme-toggle');
|
|
|
459
520
|
await writeSaasFiles(appDir);
|
|
460
521
|
}
|
|
461
522
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
if (!existsSync(join(appDir, 'AGENTS.md')) && existsSync(agentsSrc2)) {
|
|
465
|
-
await cp(agentsSrc2, join(appDir, 'AGENTS.md'));
|
|
466
|
-
}
|
|
523
|
+
// AGENTS.md is already in place via the shared `templateFiles` loop
|
|
524
|
+
// earlier in this function — no framework-root fallback needed.
|
|
467
525
|
|
|
468
526
|
// --- Git init + configure hooks directory ---
|
|
469
527
|
const { execSync } = await import('node:child_process');
|
package/package.json
CHANGED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# AGENTS.md — {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
Read this before editing any file. This is a webjs app: AI-first, web-
|
|
4
|
+
components-first, no build step. The framework's own full API reference
|
|
5
|
+
lives at https://github.com/vivek7405/webjs/blob/main/AGENTS.md — treat
|
|
6
|
+
this file as the app-scoped companion.
|
|
7
|
+
|
|
8
|
+
## Framework source is in `node_modules/`
|
|
9
|
+
|
|
10
|
+
No build step, no bundler, no minification — what you read is what
|
|
11
|
+
runs. When in doubt, grep the framework:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
node_modules/@webjskit/
|
|
15
|
+
core/ renderer, WebComponent, directives, client router,
|
|
16
|
+
Task, context, testing helpers
|
|
17
|
+
src/component.js ← lifecycle, properties, light vs shadow DOM
|
|
18
|
+
src/render-client.js ← client-side DOM patching + hydration
|
|
19
|
+
src/render-server.js ← renderToString / renderToStream
|
|
20
|
+
src/router-client.js ← Turbo-Drive-style client navigation
|
|
21
|
+
src/directives.js ← unsafeHTML, live
|
|
22
|
+
src/context.js ← Context Protocol
|
|
23
|
+
src/task.js ← async data with states
|
|
24
|
+
server/ dev + prod server, SSR, file router, actions,
|
|
25
|
+
auth, sessions, cache, rate-limit, WebSocket
|
|
26
|
+
src/ssr.js ← how metadata becomes <head> tags
|
|
27
|
+
src/router.js ← file convention → route table
|
|
28
|
+
src/actions.js ← .server.ts scanner, RPC, expose()
|
|
29
|
+
src/auth.js, session.js, cache.js, rate-limit.js, csrf.js
|
|
30
|
+
cli/ webjs CLI (dev / start / build / test / check / create / db)
|
|
31
|
+
ts-plugin/ tsserver go-to-definition for custom-element tag names
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Reaching straight for the source is the fastest way to resolve "why
|
|
35
|
+
doesn't X work?" — no documentation guesswork, no stale blog posts.
|
|
36
|
+
|
|
37
|
+
## File conventions
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
app/ thin route adapters — import from modules/
|
|
41
|
+
page.ts → /
|
|
42
|
+
layout.ts root layout, wraps every page
|
|
43
|
+
error.ts error boundary (render failures → user-friendly)
|
|
44
|
+
loading.ts Suspense fallback for sibling page
|
|
45
|
+
not-found.ts custom 404 page
|
|
46
|
+
middleware.ts global request middleware
|
|
47
|
+
[slug]/page.ts dynamic route segment
|
|
48
|
+
[...rest]/page.ts catch-all
|
|
49
|
+
(group)/ route group (parens not in URL)
|
|
50
|
+
_private/ underscore = not routable
|
|
51
|
+
api/
|
|
52
|
+
<path>/route.ts GET / POST / PUT / DELETE / WS handlers
|
|
53
|
+
sitemap.ts metadata route → /sitemap.xml
|
|
54
|
+
robots.ts metadata route → /robots.txt
|
|
55
|
+
opengraph-image.ts metadata route → /opengraph-image
|
|
56
|
+
components/ web components — extend WebComponent, call .register()
|
|
57
|
+
modules/<feature>/
|
|
58
|
+
actions/*.server.ts server actions (one function per file)
|
|
59
|
+
queries/*.server.ts data reads (one function per file)
|
|
60
|
+
components/*.ts feature-scoped components
|
|
61
|
+
utils/*.ts feature-scoped helpers
|
|
62
|
+
types.ts feature types
|
|
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
|
|
70
|
+
public/ static assets, served at /public/*
|
|
71
|
+
test/unit/*.test.ts unit tests (node --test)
|
|
72
|
+
test/browser/*.test.ts browser tests (web-test-runner)
|
|
73
|
+
middleware.ts root middleware (optional, outermost)
|
|
74
|
+
```
|
|
75
|
+
|
|
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
|
+
|
|
106
|
+
## Imports
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { html, css, WebComponent } from '@webjskit/core';
|
|
110
|
+
import '@webjskit/core/client-router'; // enable SPA nav
|
|
111
|
+
import { unsafeHTML, live } from '@webjskit/core/directives';
|
|
112
|
+
import { createContext } from '@webjskit/core/context';
|
|
113
|
+
import { Task } from '@webjskit/core/task';
|
|
114
|
+
import { fixture, waitForUpdate } from '@webjskit/core/testing';
|
|
115
|
+
|
|
116
|
+
import { rateLimit, cache, createAuth, Credentials, Session } from '@webjskit/server';
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Component pattern
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { WebComponent, html, css } from '@webjskit/core';
|
|
123
|
+
|
|
124
|
+
export class Counter extends WebComponent {
|
|
125
|
+
static tag = 'my-counter'; // required, must contain a hyphen
|
|
126
|
+
static properties = { count: { type: Number } };
|
|
127
|
+
static styles = css`button { padding: 8px 12px; }`;
|
|
128
|
+
// static shadow = true; // opt into shadow DOM (default: light DOM)
|
|
129
|
+
// static lazy = true; // download JS only when scrolled into view
|
|
130
|
+
|
|
131
|
+
render() {
|
|
132
|
+
return html`
|
|
133
|
+
<button @click=${() => this.setState({ count: this.count + 1 })}>
|
|
134
|
+
${this.count}
|
|
135
|
+
</button>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
Counter.register('my-counter');
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Server action pattern
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// modules/posts/actions/create-post.server.ts
|
|
146
|
+
'use server';
|
|
147
|
+
import { prisma } from '../../../lib/prisma.ts';
|
|
148
|
+
|
|
149
|
+
export async function createPost(input: { title: string; body: string }) {
|
|
150
|
+
if (!input.title) return { success: false, error: 'title required', status: 400 };
|
|
151
|
+
const post = await prisma.post.create({ data: input });
|
|
152
|
+
return { success: true, data: post };
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Import it from a client component — the framework rewrites it into a
|
|
157
|
+
type-safe RPC stub automatically.
|
|
158
|
+
|
|
159
|
+
## Metadata (per-page)
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
export const metadata = {
|
|
163
|
+
title: 'My page',
|
|
164
|
+
description: 'A page in {{APP_NAME}}',
|
|
165
|
+
openGraph: { type: 'website', image: 'https://...' },
|
|
166
|
+
twitter: { card: 'summary_large_image' },
|
|
167
|
+
cacheControl: 'public, max-age=60', // opt into caching (default: no-store)
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Use `generateMetadata(ctx)` when you need request-scoped values (e.g.
|
|
172
|
+
absolute URLs from `ctx.url`).
|
|
173
|
+
|
|
174
|
+
## Invariants (do not violate)
|
|
175
|
+
|
|
176
|
+
1. Custom element tags must contain a hyphen. Set `static tag`, call `.register()`.
|
|
177
|
+
2. Never import `@prisma/client` or `node:*` from client-reachable files —
|
|
178
|
+
only from `.server.ts` modules or `lib/*.ts`.
|
|
179
|
+
3. Event / property / boolean holes in `` html`` `` are unquoted:
|
|
180
|
+
`@click=${fn}`, not `@click="${fn}"`.
|
|
181
|
+
4. Use `setState()` — never mutate `this.state` directly.
|
|
182
|
+
5. Pages / layouts / metadata routes default-export a server-only function.
|
|
183
|
+
6. One exported function per action / query file. Name the file after it.
|
|
184
|
+
|
|
185
|
+
## Workflow expectations for AI agents
|
|
186
|
+
|
|
187
|
+
1. Branch before editing — never push to `main` directly.
|
|
188
|
+
2. Every code change comes with: unit test(s), AGENTS.md / docs updates if
|
|
189
|
+
the feature surface changed, `npx webjs check` passing.
|
|
190
|
+
3. Commit and push after each logical unit. No AI attribution trailers.
|
|
191
|
+
4. When unsure how a framework feature works, `grep` or `cat` the
|
|
192
|
+
relevant `node_modules/@webjskit/*/src/` file before asking the user.
|
|
193
|
+
|
|
194
|
+
Project-specific conventions and overrides live in
|
|
195
|
+
[CONVENTIONS.md](./CONVENTIONS.md).
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -82,14 +82,19 @@ variables control infrastructure — no config files needed:
|
|
|
82
82
|
|
|
83
83
|
| Environment variable | Effect |
|
|
84
84
|
|---|---|
|
|
85
|
-
| `REDIS_URL` |
|
|
85
|
+
| `REDIS_URL` | Connection string consumed by `redisStore({ url: process.env.REDIS_URL })`. Not auto-wired — call `setStore(redisStore())` once at app startup to put cache / sessions / rate-limit on Redis. |
|
|
86
86
|
| `AUTH_SECRET` | Required for auth JWT signing (32+ random chars) |
|
|
87
87
|
| `AUTH_GOOGLE_ID` | Google OAuth client ID (optional) |
|
|
88
88
|
| `AUTH_GITHUB_ID` | GitHub OAuth client ID (optional) |
|
|
89
89
|
| `PORT` | Server port (default: 3000) |
|
|
90
90
|
|
|
91
91
|
**Development:** zero env vars needed. Everything works with memory/cookie/disk.
|
|
92
|
-
**Production:** set `
|
|
92
|
+
**Production:** set `AUTH_SECRET` + `SESSION_SECRET`. For horizontal scaling, also set `REDIS_URL` and add one line at app startup:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { setStore, redisStore } from '@webjskit/server';
|
|
96
|
+
setStore(redisStore({ url: process.env.REDIS_URL }));
|
|
97
|
+
```
|
|
93
98
|
|
|
94
99
|
---
|
|
95
100
|
|