@webjsdev/cli 0.10.44 → 0.10.46
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 +2 -3
- package/bin/webjs.js +18 -18
- package/lib/api-gallery.js +1 -1
- package/lib/create.js +154 -243
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +7 -3
- package/templates/.agents/skills/webjs/SKILL.md +4 -2
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
- package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +38 -6
- package/templates/.agents/skills/webjs/references/components.md +101 -6
- package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
- package/templates/.agents/skills/webjs/references/runtime.md +1 -1
- package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
- package/templates/.agents/skills/webjs/references/styling.md +82 -2
- package/templates/AGENTS.md +12 -5
- package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
- package/templates/gallery/app/apple-icon.ts +2 -5
- package/templates/gallery/app/examples/layout.ts +16 -0
- package/templates/gallery/app/examples/todo/page.ts +2 -1
- package/templates/gallery/app/features/async-render/page.ts +3 -2
- package/templates/gallery/app/features/auth/dashboard/layout.ts +21 -0
- package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
- package/templates/gallery/app/features/auth/dashboard/page.ts +20 -0
- package/templates/gallery/app/features/auth/dashboard/settings/page.ts +22 -0
- package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
- package/templates/gallery/app/features/auth/login/page.ts +43 -0
- package/templates/gallery/app/features/auth/page.ts +34 -0
- package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
- package/templates/gallery/app/features/auth/signup/page.ts +61 -0
- package/templates/gallery/app/features/boundaries/error.ts +5 -4
- package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
- package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
- package/templates/gallery/app/features/boundaries/page.ts +11 -10
- package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
- package/templates/gallery/app/features/broadcast/page.ts +4 -3
- package/templates/gallery/app/features/caching/page.ts +5 -4
- package/templates/gallery/app/features/client-router/page.ts +7 -5
- package/templates/gallery/app/features/client-router/second/page.ts +4 -3
- package/templates/gallery/app/features/components/page.ts +3 -2
- package/templates/gallery/app/features/directives/page.ts +3 -2
- package/templates/gallery/app/features/env/page.ts +4 -3
- package/templates/gallery/app/features/file-storage/page.ts +8 -5
- package/templates/gallery/app/features/forms/page.ts +10 -6
- package/templates/gallery/app/features/frames/page.ts +26 -10
- package/templates/gallery/app/features/layout.ts +67 -0
- package/templates/gallery/app/features/metadata/page.ts +7 -6
- package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
- package/templates/gallery/app/features/rate-limit/page.ts +6 -5
- package/templates/gallery/app/features/route-handler/page.ts +4 -3
- package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
- package/templates/gallery/app/features/routing/page.ts +10 -9
- package/templates/gallery/app/features/server-actions/page.ts +7 -4
- package/templates/gallery/app/features/service-worker/page.ts +4 -3
- package/templates/gallery/app/features/sessions/page.ts +5 -4
- package/templates/gallery/app/features/stream/page.ts +46 -0
- package/templates/gallery/app/features/streaming/page.ts +32 -0
- package/templates/gallery/app/features/suspense/page.ts +35 -0
- package/templates/gallery/app/features/view-transitions/page.ts +44 -0
- package/templates/gallery/app/features/view-transitions/second/page.ts +30 -0
- package/templates/gallery/app/features/websockets/page.ts +4 -3
- package/templates/gallery/app/global-error.ts +2 -5
- package/templates/gallery/app/global-not-found.ts +4 -6
- package/templates/gallery/app/icon.ts +2 -5
- package/templates/gallery/app/manifest.ts +1 -4
- package/templates/gallery/app/opengraph-image.ts +3 -6
- package/templates/gallery/app/robots.ts +0 -3
- package/templates/gallery/app/sitemap.ts +0 -3
- package/templates/gallery/app/twitter-image.ts +3 -6
- package/templates/gallery/components/ui/badge.ts +41 -0
- package/templates/gallery/components/ui/button.ts +86 -0
- package/templates/gallery/components/ui/card.ts +36 -0
- package/templates/gallery/components/ui/input.ts +50 -0
- package/templates/gallery/lib/utils/ui.ts +31 -0
- package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
- package/templates/gallery/modules/auth/auth.server.ts +53 -0
- package/templates/gallery/modules/auth/password.server.ts +20 -0
- package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
- package/templates/gallery/modules/auth/types.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
- package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
- package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
- package/templates/gallery/modules/components/components/counter-card.ts +4 -2
- package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
- package/templates/gallery/modules/components/components/task-loader.ts +3 -2
- package/templates/gallery/modules/components/components/theme-context.ts +5 -3
- package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
- package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
- package/templates/gallery/modules/gallery/nav.ts +79 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
- package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
- package/templates/gallery/modules/server-actions/components/greeter.ts +11 -11
- package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
- package/templates/gallery/modules/stream/components/stream-demo.ts +81 -0
- package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
- package/templates/gallery/modules/streaming/components/token-stream.ts +53 -0
- package/templates/gallery/modules/suspense/components/slow-fact.ts +20 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
- package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
- package/templates/gallery/test/auth/auth.test.ts +81 -0
- package/templates/public/favicon.svg +10 -3
- package/templates/scripts/clear-api-gallery.mjs +55 -0
- package/templates/scripts/clear-gallery.mjs +143 -24
- package/lib/lean-copy.js +0 -43
- package/lib/saas-template.js +0 -568
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// one coherent theme. Prefer these tokens (and opacity modifiers like
|
|
11
11
|
// bg-primary/90) over ad-hoc colors.
|
|
12
12
|
import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
|
|
13
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
14
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
15
|
+
import { bareInputClass } from '#components/ui/input.ts';
|
|
16
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
13
17
|
import { createTodo } from '../actions/create-todo.server.ts';
|
|
14
18
|
import { toggleTodo } from '../actions/toggle-todo.server.ts';
|
|
15
19
|
import { deleteTodo } from '../actions/delete-todo.server.ts';
|
|
@@ -94,12 +98,12 @@ export class TodoApp extends WebComponent({
|
|
|
94
98
|
<!-- Add: a real <form> so it works with JS off (posts to the page action);
|
|
95
99
|
with JS, @submit intercepts and runs the optimistic path. -->
|
|
96
100
|
<form method="post" action="" @submit=${(e: SubmitEvent) => this.add(e)}
|
|
97
|
-
class="flex items-center gap-2 p-2 pl-4
|
|
101
|
+
class="${cardClass()} flex items-center gap-2 p-2 pl-4 shadow-[0_1px_0_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]">
|
|
98
102
|
<input type="hidden" name="intent" value="create" />
|
|
99
103
|
<input name="title" required maxlength="280" autocomplete="off" placeholder="What needs doing?"
|
|
100
|
-
class
|
|
104
|
+
class=${bareInputClass()} />
|
|
101
105
|
<button type="submit"
|
|
102
|
-
class="shrink-0
|
|
106
|
+
class="${buttonClass()} shrink-0">Add</button>
|
|
103
107
|
</form>
|
|
104
108
|
|
|
105
109
|
<ul class="list-none m-0 p-0 grid gap-2">
|
|
@@ -124,7 +128,7 @@ export class TodoApp extends WebComponent({
|
|
|
124
128
|
<!-- Delete: a proper icon button, revealed on row hover / focus. -->
|
|
125
129
|
<button type="submit" name="intent" value="delete" aria-label="Delete task"
|
|
126
130
|
@click=${(e: Event) => this.removeTodo(e, todo)}
|
|
127
|
-
class
|
|
131
|
+
class=${cn(buttonClass({ variant: 'destructive', size: 'none' }), 'w-7 h-7 rounded-lg opacity-0 group-hover:opacity-100 focus:opacity-100')}>
|
|
128
132
|
<svg viewBox="0 0 24 24" class="w-4 h-4 stroke-current fill-none" style="stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
129
133
|
</button>
|
|
130
134
|
</form>
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// component renders its disconnected state, so with JS off the page still reads
|
|
6
6
|
// (a live socket has no no-JS equivalent, which is the honest fallback here).
|
|
7
7
|
import { WebComponent, signal, html, connectWS, renderStream } from '@webjsdev/core';
|
|
8
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
8
10
|
|
|
9
11
|
export class WsEcho extends WebComponent {
|
|
10
12
|
private connected = signal(false);
|
|
@@ -59,9 +61,9 @@ export class WsEcho extends WebComponent {
|
|
|
59
61
|
</div>
|
|
60
62
|
<form @submit=${(e: SubmitEvent) => this.send(e)} class="flex gap-2">
|
|
61
63
|
<input name="msg" autocomplete="off" placeholder="Say something"
|
|
62
|
-
class
|
|
64
|
+
class=${inputClass('flex-1 min-w-0')} />
|
|
63
65
|
<button type="submit"
|
|
64
|
-
class
|
|
66
|
+
class=${buttonClass({ size: 'sm' })}>Send</button>
|
|
65
67
|
</form>
|
|
66
68
|
<ul class="grid gap-1.5 list-none m-0 p-0">
|
|
67
69
|
${this.lines.get().map((line) => html`
|
|
@@ -70,7 +72,7 @@ export class WsEcho extends WebComponent {
|
|
|
70
72
|
</ul>
|
|
71
73
|
<div class="grid gap-2 border-t border-border pt-4">
|
|
72
74
|
<button @click=${() => this.applyStreamUpdate()}
|
|
73
|
-
class="
|
|
75
|
+
class="${buttonClass({ variant: 'secondary', size: 'sm' })} w-fit">renderStream() an element-level update</button>
|
|
74
76
|
<ul id="ws-stream-log" class="grid gap-1.5 list-none m-0 p-0"></ul>
|
|
75
77
|
</div>
|
|
76
78
|
</div>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { createRequestHandler } from '@webjsdev/server';
|
|
7
|
+
import { testRequest, loginAndGetCookies, withSessionCookie } from '@webjsdev/server/testing';
|
|
8
|
+
|
|
9
|
+
const appDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
|
|
11
|
+
// The auth pages + dashboard middleware query the users table via Drizzle. Until
|
|
12
|
+
// `db:generate` has authored the migration (then `db:migrate` applies it, or
|
|
13
|
+
// `dev` applies it via webjs.dev.before), a request hitting those modules 500s;
|
|
14
|
+
// we detect that at the RESPONSE level (a 5xx on the dashboard) and SKIP with a
|
|
15
|
+
// clear message rather than report a misleading failure. After the db is set up
|
|
16
|
+
// every assertion runs for real.
|
|
17
|
+
process.env.DATABASE_URL ||= 'file:./dev.db';
|
|
18
|
+
process.env.AUTH_SECRET ||= 'test-secret-at-least-32-characters-long!!';
|
|
19
|
+
|
|
20
|
+
function makeHandler() {
|
|
21
|
+
// createRequestHandler builds lazily, so it succeeds even before the DB is
|
|
22
|
+
// migrated; the missing table only surfaces when a request reaches a module
|
|
23
|
+
// that queries it. That is why readiness is probed per-response.
|
|
24
|
+
return createRequestHandler({ appDir, dev: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('protected route redirects to login when unauthenticated', async (t) => {
|
|
28
|
+
const app = await makeHandler();
|
|
29
|
+
const res = await testRequest(app.handle, '/features/auth/dashboard');
|
|
30
|
+
if (res.status >= 500) {
|
|
31
|
+
t.skip('app deps not ready (run db:generate + db:migrate)');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// The dashboard middleware calls auth(req); with no session cookie it 302s to
|
|
35
|
+
// login. This needs no DB row, only a cookie read, so it is always real once
|
|
36
|
+
// the modules import.
|
|
37
|
+
assert.equal(res.status, 302, 'unauthenticated dashboard is gated');
|
|
38
|
+
assert.equal(res.headers.get('location'), '/features/auth/login');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('signup -> login -> dashboard renders for the authenticated user', async (t) => {
|
|
42
|
+
const app = await makeHandler();
|
|
43
|
+
// Probe readiness: a 5xx on the dashboard means deps/DB are not set up.
|
|
44
|
+
const probe = await testRequest(app.handle, '/features/auth/dashboard');
|
|
45
|
+
if (probe.status >= 500) { t.skip('app deps not ready; run db:generate + db:migrate'); return; }
|
|
46
|
+
|
|
47
|
+
const email = `harness+${Date.now()}@example.com`;
|
|
48
|
+
const password = 'password123';
|
|
49
|
+
|
|
50
|
+
// Real signup through the page server action (the no-JS form write-path).
|
|
51
|
+
let canSignup = true;
|
|
52
|
+
try {
|
|
53
|
+
const signupRes = await testRequest(app.handle, '/features/auth/signup', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
56
|
+
body: new URLSearchParams({ name: 'Harness', email, password }).toString(),
|
|
57
|
+
});
|
|
58
|
+
// Success auto-logs-in and 302s to the dashboard (carrying the session
|
|
59
|
+
// cookie); a 422 means validation failed. Either way the action ran.
|
|
60
|
+
assert.ok([302, 422].includes(signupRes.status), 'signup action ran');
|
|
61
|
+
if (signupRes.status === 302) assert.equal(signupRes.headers.get('location'), '/features/auth/dashboard', 'signup lands on the dashboard');
|
|
62
|
+
if (signupRes.status !== 302) canSignup = false;
|
|
63
|
+
} catch {
|
|
64
|
+
// No migrated DB table -> the action throws. Skip the DB-backed assertions.
|
|
65
|
+
canSignup = false;
|
|
66
|
+
}
|
|
67
|
+
if (!canSignup) { t.skip('no migrated DB; run db:migrate to enable the full flow'); return; }
|
|
68
|
+
|
|
69
|
+
// Real login captures the genuine signed session cookie.
|
|
70
|
+
const { cookies } = await loginAndGetCookies(app.handle, { email, password });
|
|
71
|
+
|
|
72
|
+
// With the session cookie the protected route now renders (200).
|
|
73
|
+
const dash = await testRequest(app.handle, '/features/auth/dashboard', withSessionCookie({}, cookies));
|
|
74
|
+
assert.equal(dash.status, 200, 'the session cookie unlocks the dashboard');
|
|
75
|
+
const body = await dash.text();
|
|
76
|
+
assert.match(body, /Dashboard/, 'the dashboard content rendered');
|
|
77
|
+
// The greeting interpolates the real user, so the name renders and the literal
|
|
78
|
+
// template source never leaks (a counterfactual for the escaping bug).
|
|
79
|
+
assert.match(body, /Harness/, 'the dashboard greets the signed-in user by name');
|
|
80
|
+
assert.ok(!body.includes('${user'), 'the greeting interpolation is not a literal string');
|
|
81
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="WebJs">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<!-- Matches the gallery navbar brand mark (a rounded square with a
|
|
3
|
+
foreground -> muted-foreground gradient), using the DARK-theme token
|
|
4
|
+
values (#dee2e6 -> #94989c) so it reads on a dark browser tab bar. -->
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="wj" x1="0" y1="0" x2="1" y2="1">
|
|
7
|
+
<stop offset="0" stop-color="#dee2e6"/>
|
|
8
|
+
<stop offset="1" stop-color="#94989c"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<rect width="32" height="32" rx="10" fill="url(#wj)"/>
|
|
5
12
|
</svg>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Clear the api scaffold's backend-features showcase to a clean, buildable base.
|
|
3
|
+
//
|
|
4
|
+
// The api template ships a browsable showcase (JSON/HTTP endpoints under
|
|
5
|
+
// app/api/features/, their modules, an env.ts example, and a starter test) so an
|
|
6
|
+
// agent can learn the backend idioms from real, running code. It is the api
|
|
7
|
+
// counterpart of the UI gallery's `gallery:clear`. Learn from the demos relevant
|
|
8
|
+
// to your task first (the skill at .agents/skills/webjs/ teaches the same
|
|
9
|
+
// patterns and SURVIVES this reset), then run this once to shed the showcase, then
|
|
10
|
+
// grow the api in place.
|
|
11
|
+
//
|
|
12
|
+
// It removes the showcase routes + modules + the env.ts example + the widgets
|
|
13
|
+
// test, and resets the root app/route.ts to list only the kept baseline endpoints
|
|
14
|
+
// (/api/health, /api/users). It KEEPS the agent skill, middleware.ts (CORS), the
|
|
15
|
+
// health + users endpoints, modules/users, the database wiring, and the users
|
|
16
|
+
// example test. It is a one-time reset: if the showcase is already gone (no
|
|
17
|
+
// app/api/features/) it does nothing, so a rerun never clobbers an app you built.
|
|
18
|
+
import { rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
const root = process.cwd();
|
|
22
|
+
const rm = (p) => { if (existsSync(join(root, p))) { rmSync(join(root, p), { recursive: true, force: true }); return true; } return false; };
|
|
23
|
+
|
|
24
|
+
// Guard: the showcase is identified by app/api/features/. If it is absent, the
|
|
25
|
+
// showcase was already cleared (or this is not an api scaffold), so exit before
|
|
26
|
+
// any destructive write. This keeps a rerun safe and never clobbers a built app.
|
|
27
|
+
if (!existsSync(join(root, 'app/api/features'))) {
|
|
28
|
+
console.log('No backend-features showcase found (app/api/features/ is absent); nothing to clear.');
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 1) The showcase route tree + its module + the env.ts example + the widgets test.
|
|
33
|
+
const showcasePaths = [
|
|
34
|
+
'app/api/features',
|
|
35
|
+
'modules/widgets',
|
|
36
|
+
'env.ts',
|
|
37
|
+
'test/unit/widgets.test.ts',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
let removed = 0;
|
|
41
|
+
for (const p of showcasePaths) if (rm(p)) removed++;
|
|
42
|
+
|
|
43
|
+
// 2) Reset the root app/route.ts to list only the kept baseline endpoints. The
|
|
44
|
+
// generated index also lists a `features:` block linking the showcase; strip it
|
|
45
|
+
// (and its comment) so the index does not point at routes that no longer exist.
|
|
46
|
+
// The regex preserves everything else (the app name, the health + users links).
|
|
47
|
+
const routePath = join(root, 'app/route.ts');
|
|
48
|
+
if (existsSync(routePath)) {
|
|
49
|
+
let s = readFileSync(routePath, 'utf8');
|
|
50
|
+
s = s.replace(/\n[ \t]*\/\/ The backend-features showcase[^\n]*\n[ \t]*features: \{[\s\S]*?\n[ \t]*\},\n/, '\n');
|
|
51
|
+
writeFileSync(routePath, s);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`Backend-features showcase cleared (${removed} paths removed). The agent skill, your database wiring, and the health + users endpoints are kept.`);
|
|
55
|
+
console.log('Next: build your api in app/api/ and modules/, keeping server-only code behind .server.ts.');
|
|
@@ -9,17 +9,38 @@
|
|
|
9
9
|
// same patterns and SURVIVES this reset, so nothing is lost), then run this
|
|
10
10
|
// once to shed the gallery, then grow the app in place.
|
|
11
11
|
//
|
|
12
|
-
// It
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
12
|
+
// It strips the app down to a TRULY BAREBONES blank slate: it removes the
|
|
13
|
+
// gallery routes + modules + demo metadata routes, the gallery's example design
|
|
14
|
+
// system (components/ui/), the example theme-toggle component, the example test
|
|
15
|
+
// suite (test/hello/), and every empty leftover dir, then resets app/page.ts to
|
|
16
|
+
// a minimal home AND app/layout.ts to a blank-slate base (the gallery's navbar,
|
|
17
|
+
// theme toggle, Google Fonts, AND design-token palette dropped: the reset layout
|
|
18
|
+
// carries NO design system, just the OS light/dark system colours, so the agent
|
|
19
|
+
// builds the palette as its own, guided by the skill), and drops the demo `todos`
|
|
20
|
+
// table plus the auth card's `passwordHash` column from the schema. It KEEPS only
|
|
21
|
+
// the buildable base: the agent skill (.agents/skills/webjs/), the reset root
|
|
22
|
+
// layout (the Tailwind wiring + a system-colour base, no tokens), the database
|
|
23
|
+
// wiring, the example `users` table, and `lib/utils/cn.ts` (the `webjs ui add`
|
|
24
|
+
// prerequisite).
|
|
25
|
+
//
|
|
26
|
+
// Why strip so much: the demos, the design system, and the example component /
|
|
27
|
+
// tests are all EXAMPLES to learn FROM, not a base to inherit. Leaving them
|
|
28
|
+
// nudges the implementing agent to lean on the scaffold's choices instead of
|
|
29
|
+
// building the app's own. The DURABLE knowledge is the pattern itself, which
|
|
30
|
+
// lives in the skill (.agents/skills/webjs/, re-read every session): learn it
|
|
31
|
+
// from the gallery here, then after this reset build your own (run
|
|
32
|
+
// `webjs ui add <name>` for UI primitives, cn.ts is kept for it) themed to your
|
|
33
|
+
// app. It is a one-time reset: if the gallery is already gone (no app/features/)
|
|
34
|
+
// it does nothing, so a rerun never clobbers an app you built.
|
|
35
|
+
import { rmSync, rmdirSync, existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
19
36
|
import { join } from 'node:path';
|
|
20
37
|
|
|
21
38
|
const root = process.cwd();
|
|
22
39
|
const rm = (p) => { if (existsSync(join(root, p))) { rmSync(join(root, p), { recursive: true, force: true }); return true; } return false; };
|
|
40
|
+
// Remove a directory ONLY if it exists and is empty (used to clean the empty
|
|
41
|
+
// parents a partial gallery removal leaves behind, e.g. app/api after its lone
|
|
42
|
+
// auth handler is gone). Never touches a dir that still holds files.
|
|
43
|
+
const pruneEmpty = (p) => { const abs = join(root, p); if (existsSync(abs) && readdirSync(abs).length === 0) { rmdirSync(abs); return true; } return false; };
|
|
23
44
|
|
|
24
45
|
// Guard: the gallery is identified by app/features/. If it is absent, the gallery
|
|
25
46
|
// was already cleared (or this is not a gallery scaffold), so exit before any
|
|
@@ -30,18 +51,37 @@ if (!existsSync(join(root, 'app/features'))) {
|
|
|
30
51
|
process.exit(0);
|
|
31
52
|
}
|
|
32
53
|
|
|
33
|
-
// 1) Gallery route trees + example metadata routes.
|
|
54
|
+
// 1) Gallery route trees + example metadata routes. `app/api/auth` is the auth
|
|
55
|
+
// card's createAuth handler (it lives at the app root, not under app/features/,
|
|
56
|
+
// because createAuth hardcodes /api/auth/*), and `test/auth` is the auth card's
|
|
57
|
+
// request-pipeline test, so both are removed here alongside the card.
|
|
34
58
|
const galleryPaths = [
|
|
35
|
-
'app/features', 'app/examples', 'app/sitemaps',
|
|
59
|
+
'app/features', 'app/examples', 'app/sitemaps', 'app/api/auth', 'test/auth',
|
|
36
60
|
'app/icon.ts', 'app/apple-icon.ts', 'app/manifest.ts', 'app/opengraph-image.ts',
|
|
37
61
|
'app/twitter-image.ts', 'app/robots.ts', 'app/sitemap.ts',
|
|
38
62
|
'app/global-error.ts', 'app/global-not-found.ts',
|
|
63
|
+
// The gallery's EXAMPLE design system. Removed so the reset app is a blank
|
|
64
|
+
// slate: the agent builds its own components/ui/ (run `webjs ui add`, cn.ts is
|
|
65
|
+
// kept), learning the pattern from the skill, not inheriting the gallery's.
|
|
66
|
+
'components/ui',
|
|
67
|
+
// Example scaffold artifacts (invariant 2): the theme-toggle component and the
|
|
68
|
+
// example test suite are things to learn from, not the agent's app. Removed
|
|
69
|
+
// for a true blank slate; the root layout is reset to a minimal base below
|
|
70
|
+
// (which drops its toggle wiring). The agent writes its own tests + components.
|
|
71
|
+
'components/theme-toggle.ts', 'test/hello',
|
|
72
|
+
// The gallery's markup-chunk helpers. Their only users (the feature pages +
|
|
73
|
+
// the features/examples layouts) are removed above, so drop the example file
|
|
74
|
+
// too; cn.ts + dom.ts (real infrastructure) stay under lib/utils/.
|
|
75
|
+
'lib/utils/ui.ts',
|
|
39
76
|
];
|
|
40
|
-
// 2) The gallery's feature modules (by name
|
|
77
|
+
// 2) The gallery's feature modules (by name). `auth` is the auth card's server
|
|
78
|
+
// modules (createAuth config, password hashing, signup action, current-user
|
|
79
|
+
// query), pruned with the rest of the card.
|
|
41
80
|
const galleryModules = [
|
|
42
|
-
'async-render', 'broadcast', 'caching', 'client-router', 'components',
|
|
43
|
-
'directives', 'file-storage', 'frames', 'optimistic-ui', 'rate-limit',
|
|
44
|
-
'route-handler', 'server-actions', 'sessions', '
|
|
81
|
+
'async-render', 'auth', 'broadcast', 'caching', 'client-router', 'components',
|
|
82
|
+
'directives', 'file-storage', 'frames', 'gallery', 'optimistic-ui', 'rate-limit',
|
|
83
|
+
'route-handler', 'server-actions', 'sessions', 'stream', 'streaming', 'suspense',
|
|
84
|
+
'todo', 'websockets',
|
|
45
85
|
].map((m) => `modules/${m}`);
|
|
46
86
|
|
|
47
87
|
let removed = 0;
|
|
@@ -50,26 +90,65 @@ for (const p of [...galleryPaths, ...galleryModules]) if (rm(p)) removed++;
|
|
|
50
90
|
// 3) Reset app/page.ts to a minimal home (no gallery grid, no dead links).
|
|
51
91
|
writeFileSync(join(root, 'app/page.ts'), MINIMAL_PAGE());
|
|
52
92
|
|
|
53
|
-
// 4) Drop the demo `todos` table
|
|
93
|
+
// 4) Drop the demo `todos` table and the auth card's `passwordHash` column from
|
|
94
|
+
// the schema (keep the example `users` table and everything else), reverting the
|
|
95
|
+
// schema to the minimal base.
|
|
54
96
|
const schemaPath = join(root, 'db/schema.server.ts');
|
|
55
97
|
if (existsSync(schemaPath)) {
|
|
56
98
|
let s = readFileSync(schemaPath, 'utf8');
|
|
57
99
|
s = s.replace(/\n(?:\/\/[^\n]*\n)*export const todos = table\('todos',[\s\S]*?\n\}\);\n/, '\n');
|
|
58
100
|
s = s.replace(/defineRelations\(\{ users, todos \}/, 'defineRelations({ users }');
|
|
101
|
+
// Strip the auth `passwordHash` column (and its leading comment lines) from the
|
|
102
|
+
// users table; the rest of the table is the minimal example base.
|
|
103
|
+
s = s.replace(/\n(?:[ \t]*\/\/[^\n]*\n)*[ \t]*passwordHash: text\(\),\n/, '\n');
|
|
59
104
|
writeFileSync(schemaPath, s);
|
|
60
105
|
}
|
|
61
106
|
|
|
62
|
-
// 5)
|
|
107
|
+
// 5) Reset the root layout to a blank-slate base. The gallery layout ships a
|
|
108
|
+
// navbar, a theme toggle, Google Fonts, and a full design-token palette; a blank
|
|
109
|
+
// slate should inherit NONE of that, so OVERWRITE it with a minimal layout that
|
|
110
|
+
// carries no design system: the Tailwind stylesheet + OS light/dark system
|
|
111
|
+
// colours (Canvas / CanvasText) + a bare <main>. The agent then builds the
|
|
112
|
+
// palette (CSS tokens + @theme + a light-dark() theme) as its own, guided by
|
|
113
|
+
// .agents/skills/webjs/references/styling.md.
|
|
114
|
+
// GUARD: overwrite only while the layout still carries the gallery's own navbar
|
|
115
|
+
// brand. A hand-customised layout (the brand removed / replaced) is the user's
|
|
116
|
+
// work, so it is KEPT and only the dangling references to the removed
|
|
117
|
+
// theme-toggle file are stripped surgically.
|
|
118
|
+
const layoutPath = join(root, 'app/layout.ts');
|
|
119
|
+
if (existsSync(layoutPath)) {
|
|
120
|
+
const current = readFileSync(layoutPath, 'utf8');
|
|
121
|
+
if (current.includes('WebJs Gallery')) {
|
|
122
|
+
writeFileSync(layoutPath, MINIMAL_LAYOUT());
|
|
123
|
+
} else {
|
|
124
|
+
console.log('app/layout.ts looks customised; keeping it (only the theme-toggle wiring is stripped).');
|
|
125
|
+
writeFileSync(
|
|
126
|
+
layoutPath,
|
|
127
|
+
current
|
|
128
|
+
.replace(/^import '#components\/theme-toggle\.ts';\n/m, '')
|
|
129
|
+
.replace(/^\s*<theme-toggle><\/theme-toggle>\n?/m, ''),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 6) Drop generated migrations + the dev database so the next db:generate is
|
|
63
135
|
// clean against the reset schema (safe: the scaffold has no real data yet).
|
|
64
136
|
rm('db/migrations');
|
|
65
137
|
for (const f of ['db/dev.db', 'db/dev.db-shm', 'db/dev.db-wal']) rm(f);
|
|
66
138
|
|
|
67
|
-
|
|
139
|
+
// 7) Prune the empty leftover dirs a partial removal left behind, so the reset
|
|
140
|
+
// tree is a clean blank slate: app/api (held only the gallery auth handler),
|
|
141
|
+
// test/unit + test/e2e (empty base placeholders), and test/ itself once its
|
|
142
|
+
// example suites are gone. modules/ + components/ are kept as empty build
|
|
143
|
+
// targets (the reset home + `webjs ui add` land there). Order matters: prune the
|
|
144
|
+
// children before test/ so it reads as empty.
|
|
145
|
+
for (const d of ['app/api', 'test/unit', 'test/e2e', 'test']) if (pruneEmpty(d)) removed++;
|
|
146
|
+
|
|
147
|
+
console.log(`Gallery cleared (${removed} paths removed). The agent skill and your database wiring are kept. Build your own design system: run \`npx webjsdev ui add <name>\` and theme it (see .agents/skills/webjs/references/styling.md).`);
|
|
68
148
|
console.log('Next: regenerate the database (db:generate then db:migrate), then start the dev server and build your app in app/ and modules/.');
|
|
69
149
|
|
|
70
150
|
function MINIMAL_PAGE() {
|
|
71
151
|
return `import { html } from '@webjsdev/core';
|
|
72
|
-
import '#components/theme-toggle.ts';
|
|
73
152
|
|
|
74
153
|
export const metadata = {
|
|
75
154
|
title: 'Home',
|
|
@@ -77,19 +156,59 @@ export const metadata = {
|
|
|
77
156
|
|
|
78
157
|
export default function Home() {
|
|
79
158
|
return html\`
|
|
80
|
-
<div class="fixed top-4 right-4 z-10"><theme-toggle></theme-toggle></div>
|
|
81
159
|
<div class="max-w-2xl mx-auto px-6 py-24 flex flex-col items-center text-center gap-6">
|
|
82
160
|
<h1 class="text-4xl font-bold tracking-tight m-0">Your app</h1>
|
|
83
|
-
<p class="text-base
|
|
84
|
-
The gallery is cleared. This is <code class="text-[0.9em]
|
|
85
|
-
app from here. The guide is <code class="text-[0.9em]
|
|
161
|
+
<p class="text-base leading-relaxed m-0 opacity-70">
|
|
162
|
+
The gallery is cleared. This is <code class="text-[0.9em]">app/page.ts</code>. Build your
|
|
163
|
+
app from here. The guide is <code class="text-[0.9em]">.agents/skills/webjs/SKILL.md</code>.
|
|
86
164
|
</p>
|
|
87
|
-
<nav class="flex items-center gap-5 text-sm
|
|
88
|
-
<a href="https://docs.webjs.dev" class="hover:
|
|
89
|
-
<a href="https://github.com/webjsdev/webjs" class="hover:
|
|
165
|
+
<nav class="flex items-center gap-5 text-sm opacity-70">
|
|
166
|
+
<a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="hover:opacity-100 transition-opacity no-underline">Docs</a>
|
|
167
|
+
<a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="hover:opacity-100 transition-opacity no-underline">GitHub</a>
|
|
90
168
|
</nav>
|
|
91
169
|
</div>
|
|
92
170
|
\`;
|
|
93
171
|
}
|
|
94
172
|
`;
|
|
95
173
|
}
|
|
174
|
+
|
|
175
|
+
function MINIMAL_LAYOUT() {
|
|
176
|
+
return `import { html } from '@webjsdev/core';
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Root layout: the ONLY file that writes the document shell. It links the
|
|
180
|
+
* Tailwind stylesheet and renders \${children} in a bare container. This is a
|
|
181
|
+
* BLANK SLATE with no design system: it uses the OS light/dark system colours
|
|
182
|
+
* (Canvas / CanvasText) so it reads fine immediately, and it is where YOU build
|
|
183
|
+
* the app's look. The recommended setup, CSS custom-property tokens mapped into
|
|
184
|
+
* Tailwind via @theme (so bg-background / text-foreground work), a DRY
|
|
185
|
+
* light-dark() palette, a header/nav, and the ui class helpers, is taught in
|
|
186
|
+
* .agents/skills/webjs/references/styling.md. Run \`npx webjsdev ui add <name>\`
|
|
187
|
+
* to pull primitives, then theme them here.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
// Favicon via metadata.icons so the framework emits the <link> into <head> (a
|
|
191
|
+
// hand-written <link> in the template body is ignored by browsers).
|
|
192
|
+
export const metadata = { icons: '/public/favicon.svg' };
|
|
193
|
+
|
|
194
|
+
export default function RootLayout({ children }: { children: unknown }) {
|
|
195
|
+
return html\`
|
|
196
|
+
<meta name="color-scheme" content="light dark">
|
|
197
|
+
<link rel="stylesheet" href="/public/tailwind.css">
|
|
198
|
+
<style>
|
|
199
|
+
html, body { margin: 0; }
|
|
200
|
+
body {
|
|
201
|
+
background: Canvas;
|
|
202
|
+
color: CanvasText;
|
|
203
|
+
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
204
|
+
-webkit-font-smoothing: antialiased;
|
|
205
|
+
-moz-osx-font-smoothing: grayscale;
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
208
|
+
<main class="min-h-dvh max-w-3xl mx-auto px-6 py-10">
|
|
209
|
+
\${children}
|
|
210
|
+
</main>
|
|
211
|
+
\`;
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
}
|
package/lib/lean-copy.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The scaffold's lean-copy of a ui component (#983).
|
|
3
|
-
*
|
|
4
|
-
* `webjs create` copies a few `@webjsdev/ui` registry components into a
|
|
5
|
-
* generated app. To match what `webjs ui add` writes, a Tier-1 helper's worked
|
|
6
|
-
* `@example` is stripped (the example is served on demand by `webjs ui view` /
|
|
7
|
-
* the MCP `ui` tool), while a Tier-2 element file is kept whole. Both scaffold
|
|
8
|
-
* copiers (`create.js` and `saas-template.js`) go through THIS one helper so
|
|
9
|
-
* they cannot drift.
|
|
10
|
-
*
|
|
11
|
-
* The strip primitives live in `@webjsdev/ui/registry/extract`; if that subpath
|
|
12
|
-
* cannot be resolved, this degrades to a no-op (keep the example) so the strip
|
|
13
|
-
* is never a reason `webjs create` fails.
|
|
14
|
-
*
|
|
15
|
-
* @module lean-copy
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
let _mod = null;
|
|
19
|
-
|
|
20
|
-
async function loadPrimitives() {
|
|
21
|
-
if (_mod) return _mod;
|
|
22
|
-
try {
|
|
23
|
-
const m = await import('@webjsdev/ui/registry/extract');
|
|
24
|
-
_mod = { stripExample: m.stripExample, isCustomElementSource: m.isCustomElementSource };
|
|
25
|
-
} catch {
|
|
26
|
-
_mod = { stripExample: (s) => s, isCustomElementSource: () => true };
|
|
27
|
-
}
|
|
28
|
-
return _mod;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Return the component source as `webjs ui add` would write it: a Tier-1 helper
|
|
33
|
-
* has its worked `@example` stripped and a pointer left; a Tier-2 element is
|
|
34
|
-
* returned unchanged.
|
|
35
|
-
*
|
|
36
|
-
* @param {string} source the component source (imports already rewritten)
|
|
37
|
-
* @param {string} name the component name (for the pointer)
|
|
38
|
-
* @returns {Promise<string>}
|
|
39
|
-
*/
|
|
40
|
-
export async function leanComponentSource(source, name) {
|
|
41
|
-
const { stripExample, isCustomElementSource } = await loadPrimitives();
|
|
42
|
-
return isCustomElementSource(source) ? source : stripExample(source, name);
|
|
43
|
-
}
|