@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.
Files changed (111) hide show
  1. package/README.md +2 -3
  2. package/bin/webjs.js +18 -18
  3. package/lib/api-gallery.js +1 -1
  4. package/lib/create.js +154 -243
  5. package/package.json +1 -1
  6. package/templates/.agents/rules/workflow.md +7 -3
  7. package/templates/.agents/skills/webjs/SKILL.md +4 -2
  8. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
  9. package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
  10. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +38 -6
  11. package/templates/.agents/skills/webjs/references/components.md +101 -6
  12. package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
  13. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
  14. package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
  15. package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
  16. package/templates/.agents/skills/webjs/references/runtime.md +1 -1
  17. package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
  18. package/templates/.agents/skills/webjs/references/styling.md +82 -2
  19. package/templates/AGENTS.md +12 -5
  20. package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
  21. package/templates/gallery/app/apple-icon.ts +2 -5
  22. package/templates/gallery/app/examples/layout.ts +16 -0
  23. package/templates/gallery/app/examples/todo/page.ts +2 -1
  24. package/templates/gallery/app/features/async-render/page.ts +3 -2
  25. package/templates/gallery/app/features/auth/dashboard/layout.ts +21 -0
  26. package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
  27. package/templates/gallery/app/features/auth/dashboard/page.ts +20 -0
  28. package/templates/gallery/app/features/auth/dashboard/settings/page.ts +22 -0
  29. package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
  30. package/templates/gallery/app/features/auth/login/page.ts +43 -0
  31. package/templates/gallery/app/features/auth/page.ts +34 -0
  32. package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
  33. package/templates/gallery/app/features/auth/signup/page.ts +61 -0
  34. package/templates/gallery/app/features/boundaries/error.ts +5 -4
  35. package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
  36. package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
  37. package/templates/gallery/app/features/boundaries/page.ts +11 -10
  38. package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
  39. package/templates/gallery/app/features/broadcast/page.ts +4 -3
  40. package/templates/gallery/app/features/caching/page.ts +5 -4
  41. package/templates/gallery/app/features/client-router/page.ts +7 -5
  42. package/templates/gallery/app/features/client-router/second/page.ts +4 -3
  43. package/templates/gallery/app/features/components/page.ts +3 -2
  44. package/templates/gallery/app/features/directives/page.ts +3 -2
  45. package/templates/gallery/app/features/env/page.ts +4 -3
  46. package/templates/gallery/app/features/file-storage/page.ts +8 -5
  47. package/templates/gallery/app/features/forms/page.ts +10 -6
  48. package/templates/gallery/app/features/frames/page.ts +26 -10
  49. package/templates/gallery/app/features/layout.ts +67 -0
  50. package/templates/gallery/app/features/metadata/page.ts +7 -6
  51. package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
  52. package/templates/gallery/app/features/rate-limit/page.ts +6 -5
  53. package/templates/gallery/app/features/route-handler/page.ts +4 -3
  54. package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
  55. package/templates/gallery/app/features/routing/page.ts +10 -9
  56. package/templates/gallery/app/features/server-actions/page.ts +7 -4
  57. package/templates/gallery/app/features/service-worker/page.ts +4 -3
  58. package/templates/gallery/app/features/sessions/page.ts +5 -4
  59. package/templates/gallery/app/features/stream/page.ts +46 -0
  60. package/templates/gallery/app/features/streaming/page.ts +32 -0
  61. package/templates/gallery/app/features/suspense/page.ts +35 -0
  62. package/templates/gallery/app/features/view-transitions/page.ts +44 -0
  63. package/templates/gallery/app/features/view-transitions/second/page.ts +30 -0
  64. package/templates/gallery/app/features/websockets/page.ts +4 -3
  65. package/templates/gallery/app/global-error.ts +2 -5
  66. package/templates/gallery/app/global-not-found.ts +4 -6
  67. package/templates/gallery/app/icon.ts +2 -5
  68. package/templates/gallery/app/manifest.ts +1 -4
  69. package/templates/gallery/app/opengraph-image.ts +3 -6
  70. package/templates/gallery/app/robots.ts +0 -3
  71. package/templates/gallery/app/sitemap.ts +0 -3
  72. package/templates/gallery/app/twitter-image.ts +3 -6
  73. package/templates/gallery/components/ui/badge.ts +41 -0
  74. package/templates/gallery/components/ui/button.ts +86 -0
  75. package/templates/gallery/components/ui/card.ts +36 -0
  76. package/templates/gallery/components/ui/input.ts +50 -0
  77. package/templates/gallery/lib/utils/ui.ts +31 -0
  78. package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
  79. package/templates/gallery/modules/auth/auth.server.ts +53 -0
  80. package/templates/gallery/modules/auth/password.server.ts +20 -0
  81. package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
  82. package/templates/gallery/modules/auth/types.ts +9 -0
  83. package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
  84. package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
  85. package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
  86. package/templates/gallery/modules/components/components/counter-card.ts +4 -2
  87. package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
  88. package/templates/gallery/modules/components/components/task-loader.ts +3 -2
  89. package/templates/gallery/modules/components/components/theme-context.ts +5 -3
  90. package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
  91. package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
  92. package/templates/gallery/modules/gallery/nav.ts +79 -0
  93. package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
  94. package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
  95. package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
  96. package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
  97. package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
  98. package/templates/gallery/modules/server-actions/components/greeter.ts +11 -11
  99. package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
  100. package/templates/gallery/modules/stream/components/stream-demo.ts +81 -0
  101. package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
  102. package/templates/gallery/modules/streaming/components/token-stream.ts +53 -0
  103. package/templates/gallery/modules/suspense/components/slow-fact.ts +20 -0
  104. package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
  105. package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
  106. package/templates/gallery/test/auth/auth.test.ts +81 -0
  107. package/templates/public/favicon.svg +10 -3
  108. package/templates/scripts/clear-api-gallery.mjs +55 -0
  109. package/templates/scripts/clear-gallery.mjs +143 -24
  110. package/lib/lean-copy.js +0 -43
  111. 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 rounded-2xl bg-card border border-border shadow-[0_1px_0_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]">
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="flex-1 min-w-0 bg-transparent border-0 outline-none text-foreground text-[15px] placeholder:text-muted-foreground py-1.5" />
104
+ class=${bareInputClass()} />
101
105
  <button type="submit"
102
- class="shrink-0 px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm border-0 cursor-pointer transition-all hover:bg-primary/90 active:scale-[0.97]">Add</button>
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="shrink-0 grid place-items-center w-7 h-7 rounded-lg border-0 bg-transparent text-muted-foreground cursor-pointer opacity-0 group-hover:opacity-100 focus:opacity-100 transition-all hover:text-destructive hover:bg-[color-mix(in_oklch,var(--color-destructive)_12%,transparent)]">
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="flex-1 px-3 py-2 rounded-xl bg-card border border-border text-foreground text-sm outline-none focus:border-border-strong" />
64
+ class=${inputClass('flex-1 min-w-0')} />
63
65
  <button type="submit"
64
- class="px-3.5 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm border-0 cursor-pointer transition-all hover:bg-primary/90 active:scale-[0.97]">Send</button>
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="w-fit px-3.5 py-1.5 rounded-xl bg-card border border-border text-foreground text-sm cursor-pointer transition-colors hover:border-border-strong">renderStream() an element-level update</button>
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
- <rect width="32" height="32" rx="7" fill="#e9edf0"/>
3
- <path d="M8 11.5l4.5 4.5L8 20.5" fill="none" stroke="#1e2226" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>
4
- <line x1="15.5" y1="21.5" x2="23" y2="21.5" stroke="#1e2226" stroke-width="2.6" stroke-linecap="round"/>
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 removes the gallery routes + modules + demo metadata routes, resets
13
- // app/page.ts to a minimal home, and drops the demo `todos` table from the
14
- // schema. It KEEPS the agent skill (.agents/skills/webjs/), the layout, the
15
- // database wiring, the theme toggle, and (for the saas template) the auth
16
- // modules. It is a one-time reset: if the gallery is already gone (no
17
- // app/features/) it does nothing, so a rerun never clobbers an app you built.
18
- import { rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
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, so saas auth modules survive).
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', 'todo', 'websockets',
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 from the schema (keep everything else).
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) Drop generated migrations + the dev database so the next db:generate is
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
- console.log(`Gallery cleared (${removed} paths removed). The agent skill and your database wiring are kept.`);
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 text-muted-foreground leading-relaxed m-0">
84
- The gallery is cleared. This is <code class="text-[0.9em] text-foreground">app/page.ts</code>. Build your
85
- app from here. The guide is <code class="text-[0.9em] text-foreground">.agents/skills/webjs/SKILL.md</code>.
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 text-muted-foreground">
88
- <a href="https://docs.webjs.dev" class="hover:text-foreground transition-colors no-underline">Docs</a>
89
- <a href="https://github.com/webjsdev/webjs" class="hover:text-foreground transition-colors no-underline">GitHub</a>
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
- }