Skip to content

feat(gallery): promote scaffold gallery to root gallery/ app - #1371

Merged
vivek7405 merged 13 commits into
mainfrom
feat/root-gallery-app
Aug 10, 2026
Merged

feat(gallery): promote scaffold gallery to root gallery/ app#1371
vivek7405 merged 13 commits into
mainfrom
feat/root-gallery-app

Conversation

@vivek7405

Copy link
Copy Markdown
Collaborator

Closes #1370

Promotes the scaffold gallery from packages/cli/templates/gallery/ to a first-class, top-level runnable WebJs application in /gallery at the root of the monorepo.

Summary of changes

  • New root workspace app at gallery/ (@webjsdev/gallery) with full routes, components, modules, and tests.
  • Single source of truth pattern using scripts/sync-scaffold-gallery.mjs for prepack/postpack sync.
  • Updated packages/cli/lib/create.js to source templates directly from gallery/ in dev mode.

Test Plan

  • Run webjs check inside gallery/.
  • Run unit and browser tests inside gallery/.
  • Verify webjs create scaffolding works cleanly.

@vivek7405 vivek7405 self-assigned this Aug 9, 2026

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: root gallery app architecture, templates & CLI resolution

The promotion of the scaffold gallery to a top-level runnable app in /gallery cleanly establishes a single source of truth. Scaffolding, CLI prepack sync, and test suites are all aligned.

Comment thread packages/cli/lib/create.js Outdated
@vivek7405
vivek7405 marked this pull request as ready for review August 9, 2026 20:17
The new gallery/ workspace was never recorded, so every CI job died at
`npm ci` with "can only install packages when your package.json and
package-lock.json are in sync".
Promoting the gallery to a runnable root app gave it a root layout, a home
page, a theme toggle, and cn.ts. copyGallery() copies app/, components/ and
lib/ wholesale, so those four landed in every scaffolded app and the branch
then skipped the scaffold's own versions to avoid the clobber.

That silently downgraded 'webjs create': the generated layout lost
displayName, cspNonce(), LayoutProps typing and the metadata.icons favicon,
and cn.ts stopped being the verbatim @webjsdev/ui registry copy that
'webjs ui add' depends on.

Filter the four out of both the copy and the prepack bundle instead, from one
shared list, so monorepo-dev and installed-npm scaffolding emit byte-identical
apps and the scaffold keeps writing its own shell. Gitignore the transient
prepack bundle so it can never be committed back as a second source.
The new home page called cardClass({ interactive: true }) (it takes a class
string), badgeClass({ variant: 'secondary' }) (only default and outline
exist), and read f.category / f.description off NavItem, which carries
neither. Every card rendered two undefined holes, and webjs typecheck was
red on six errors.

Flatten FEATURE_GROUPS in the render function so a card can label itself
with the group it came from, and read the blurb the model actually has.

Also refresh cn.ts from the @webjsdev/ui registry: the file arrived as a
copy of the blog's older one, which is the drift promoting the gallery is
meant to end.
The gallery is promoted so framework changes get validated against it, but
nothing ran it: its test:browser pointed at a bare wtr with no config and no
runner dependency, and its node suite was in no job.

Ship the scaffold's web-test-runner config, add the runner deps, and run
typecheck plus 'webjs test' (node + browser) for gallery/ in the in-repo app
job, next to website and blog. Migrate its database there first, because the
auth test SKIPS rather than fails on a missing users table and would have
gone green vacuously.

Exclude **/browser/** from its tsconfig: those files run under the runner's
mocha tdd globals, which are not in the app's type surface.

Teach require-scaffold-with-src.sh the new gallery/ path, or the gate stops
seeing a gallery demo as a teaching surface.
The image copied website and examples but not gallery/, so the artifact
every service deploys could not serve it, and npm install inside the image
had no manifest for the new workspace.

Copy its manifest and source, compile its Tailwind alongside the others, and
add the compose service (port 15005, its own SQLite volume) so local parity
matches the intended gallery.webjs.dev service.

Verified by building the image and booting the gallery from it: /__webjs/ready,
the home page, a feature card, the auth card, and the todo example all 200,
and the stylesheet is baked in.
gallery/ is now the canonical source of what every scaffolded app ships, so
an agent editing it needs the rules that are specific to this directory: the
generated templates copy is never authored, four app-shell files are not
payload, and the gallery:clear + coverage-gate obligations that come with a
new demo.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Review + readiness pass

Picked this up to get it mergeable. Every one of the 9 required checks was failing, and the review turned up a scaffold regression that would have shipped silently. Six commits on top.

Blockers fixed

1. npm ci failed in every job. The gallery workspace was added to the root package.json but never recorded in package-lock.json, so all nine jobs died in setup with "can only install packages when your package.json and package-lock.json are in sync". The lockfile change is additive only: the gallery workspace entry, its link, and a nested @types/node / undici-types pair.

2. webjs create silently downgraded. This is the one worth reading.

Promoting the gallery to a runnable app gave it app/layout.ts, app/page.ts, components/theme-toggle.ts, and lib/utils/cn.ts. copyGallery() copies app/, components/, and lib/ wholesale, so all four landed in every scaffolded app, and the branch resolved the clobber by making the generator skip its own versions (if (!existsSync(join(appDir, 'app', 'layout.ts')))).

The generator's own docstring stated the opposite invariant and was left in place: "The scaffold's own app/page.ts / app/layout.ts are written AFTER this and the gallery ships neither, so there is no clobber."

What a generated app lost: displayName in the title, cspNonce() on the theme script, LayoutProps typing, the metadata.icons favicon, the teaching comments, the .dark class sync @webjsdev/ui components key off, and a cn.ts that was no longer the verbatim registry copy webjs ui add depends on (the gallery's is a stale 397-line fork of the blog's; the registry is at 434).

Fixed by filtering those four out of both the copy and the prepack bundle, from one shared list (packages/cli/lib/gallery-shell-files.js), rather than skipping the generator. Verified: scaffolding from the repo-root gallery and from the prepack bundle now produce byte-identical apps (diff -r clean), and the generated layout is the scaffold's again.

3. The gallery home page was written against APIs that do not exist. cardClass({ interactive: true }) (it takes a class string), badgeClass({ variant: 'secondary' }) (only default and outline exist), and f.category / f.description off NavItem, which carries neither. Every card rendered two undefined holes and webjs typecheck was red on six errors. Now flattens FEATURE_GROUPS so a card labels itself with its real group, and reads blurb.

Gaps closed against the issue's acceptance criteria

  • Nothing ran the gallery. test:browser pointed at a bare wtr with no config and no runner dependency, and its node suite was in no job. Added the scaffold's web-test-runner.config.js, the runner deps, and a typecheck + webjs test (node + browser) step in the in-repo app job. Its database is migrated there first, because test/auth/auth.test.ts skips rather than fails on a missing users table and would have gone green vacuously.
  • The deploy artifact could not serve it. The Dockerfile copied website and examples but not gallery/, and had no manifest for the new workspace. Added the copies, the Tailwind compile, and a compose service on 15005.
  • .env.example was missing (its own .gitignore negates one), root dev:gallery / test:browser:gallery scripts, and gallery/AGENTS.md.
  • require-scaffold-with-src.sh matched only packages/cli/templates/, so after the move it no longer recognised a gallery demo as a teaching surface. Taught it the new path, with the test updated.
  • The transient prepack bundle at packages/cli/templates/gallery/ was not gitignored, so a local prepack left 130+ untracked files inviting a commit back into a second source. Now ignored.
  • gallery-coverage.test.js preferred the bundle when present and fell back to the canonical source. Inverted: it always gates gallery/, since gating a stale bundle is the drift this test exists to catch.

Doc surfaces synced: framework-dev.md, README.md, gallery/AGENTS.md, and the three .claude/skills/ that still pointed at packages/cli/templates/gallery/**.

Verification

  • npm test: 4271/4277. The 5 failures are the known linked-worktree set (2 Bun listener, 3 differential-elision) that pass in a primary checkout and in CI.
  • webjs check + webjs doctor: clean on examples/blog, gallery, website.
  • gallery: typecheck clean, node suite 2/2, browser suite 4/4 in Chromium.
  • Docker image built, gallery booted from it: /__webjs/ready, /, /features/forms, /features/auth, /examples/todo, /features/components all 200, stylesheet baked in, zero undefined in the rendered home page.
  • prepack → scaffold → postpack round trip: bundle excludes the four shell files, scaffolds identically to the repo-root source, --clean removes it.

Left out, deliberately

gallery.webjs.dev still needs its Railway service and DNS provisioned. The repo side is ready (image, compose service, readiness gate all verified); creating the service is an ops action outside this PR.

One pre-existing gap this surfaced, not introduced here and not fixed here: a freshly scaffolded app's webjs typecheck is red on its own test/hello/browser/hello.test.js, because the generated tsconfig.json includes test/**/* with checkJs: true while browser tests run under the runner's mocha tdd globals. The gallery works around it with a **/browser/** exclude. Worth its own issue if you want it tracked.

Five defects from the review of the readiness commits.

The compose gallery service set only AUTH_SECRET, but the image sets
NODE_ENV=production and the sessions card throws at module load without
SESSION_SECRET, so every route under the sessions segment returned a 500
from the deploy artifact. The blog service two blocks above already sets
both, for exactly this reason. FILE_URL_SECRET rides along because the
file-storage card signs its download links and otherwise falls back to a
dev value in production.

.env.example documented neither, which is why the compose omission was
easy to make, and gallery/AGENTS.md tells the reader that copying it is
the whole first-run setup.

hover:border-border-strong compiled to nothing: Tailwind v4 resolves
border-<name> from a --color-<name> theme key, and the gallery's @theme
block never mapped one, so the hover state did not exist on the home
cards, the secondary button, or the todo rows. The generated scaffold
already emits this mapping; only the gallery's own block was missing it.

The home page hoisted a cardClass() call to module scope. That is a
module side effect, so the page shipped to the browser with no client
behaviour to justify it. Moving it into the render function fixes the
half this PR introduced (nav.ts keeps the page shipping for a separate,
pre-existing reason).

npm run dev never started the gallery, so the port table added for it
described a server that did not exist. It starts now, with a
GALLERY_PORT override matching the other two apps.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Second review round, scoped to the six readiness commits (850536e6..a62a27c5)

Those commits fixed the first round's findings but were themselves unreviewed, so this round covered only them. Five findings, all fixed in 651b082.

1. The deploy image 500s on the sessions card. The new compose service set only AUTH_SECRET, but the Dockerfile sets NODE_ENV=production and modules/sessions/session-config.server.ts throws SESSION_SECRET must be set in production at module load. The segment middleware imports it, so every URL under /features/sessions returned a 500 from the artifact this PR added. The blog service two blocks above sets both. Added SESSION_SECRET and FILE_URL_SECRET (the file-storage card signs download links and was otherwise falling back to a dev secret in production). Verified with docker compose config.

2. .env.example documented neither var, while gallery/AGENTS.md presents cp .env.example .env as the whole first-run setup. The dev fallbacks hide it locally, which is how finding 1 got made. Both added.

3. hover:border-border-strong compiled to nothing. Tailwind v4 resolves border-<name> from a --color-<name> key; the gallery's @theme block maps --color-border, never --color-border-strong, though app/layout.ts:69 defines the raw --border-strong. The hover state silently did not exist on the home cards, components/ui/button.ts:49, or modules/todo/components/todo-app.ts:116,125. One @theme line fixes all four. The generated scaffold already emits it (create.js:1165); only the gallery's own block was missing it. Verified: public/tailwind.css now carries hover\:border-border-strong:hover{border-color:var(--color-border-strong)}.

4. The home page hoisted cardClass() to module scope, a module side effect that ships a page with no client behaviour. Moved into Home(). This does not by itself restore elision (see below).

5. npm run dev never started the gallery, so the port table this PR added to README.md described a server that did not exist and the prose still said "both apps". scripts/dev-all.js starts it now on a GALLERY_PORT override, matching the other two.

One pre-existing issue this surfaced, NOT fixed here

app/page.ts still ships to the browser after finding 4. webjs elision now names a different blocker:

{"file":"app/page.ts","verdict":"shipped","blocker":"modules/gallery/nav.ts"}

modules/gallery/nav.ts:79 is export const FEATURES: NavItem[] = FEATURE_GROUPS.flatMap((g) => g.items), a top-level call and therefore a module side effect. Counterfactual run with the fix stashed: shipped before and after, so finding 4 removed one of two causes.

That line predates this PR (it came in with a35e1f5 / #1060) and is unchanged here, and modules/ is copied into generated apps, so every scaffolded app's home page ships whole for the same reason. Fixing it changes scaffold output and needs the generate-boot-check loop, so it is out of this PR's scope and left for the owner to call.

Verification

  • webjs check: clean. webjs typecheck: clean.
  • Gallery suite: node + browser, 4 passed in Chromium.
  • test/scaffolds/*.test.js: 65/65.
  • docker compose config: all three secrets resolve on the gallery service.
  • No scaffold surface applies to these six files: app/page.ts and lib/utils/cn.ts are GALLERY_APP_SHELL_FILES (filtered from both the copy and the prepack bundle), and copyGallery() / sync-scaffold-gallery.mjs copy only app, modules, components, lib, test, so public/input.css and .env.example never reach a generated app.

The home page shipped to the browser in the gallery AND in every app
webjs create generates, for a page with no client behaviour at all. It
dragged card.ts, badge.ts, nav.ts and the ~400-line cn.ts along with it.

Two independent causes, both in modules/gallery/nav.ts.

FEATURES was a const initialised by a top-level FEATURE_GROUPS.flatMap()
call. A top-level call is a module side effect, so every importer was
pinned. It is a featureList() function now, called inside the render
function, and the generated home page calls it there too.

The second cause was the word "window" in the rate-limit blurb prose
("Retry-After past the window"). nav.ts is neither a route module nor a
component, so the elision analyser scans it with string literals intact
and the client-global pattern matched ordinary English inside a quoted
string. Reworded to "once the interval resets".

Verified: gallery app/page.ts goes shipped -> inert, and a freshly
generated app boots with app/page.ts absent from its boot script, 25
feature cards, and no undefined holes.

The analyser treating prose inside a string literal as a browser-global
reference is a framework-side false positive that outlives this reword.
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Deferred finding fixed in-place (9a19005)

The elision issue reported as out-of-scope above is fixed here rather than tracked separately. Investigating it showed my earlier diagnosis was only half right: there were two independent causes, both in modules/gallery/nav.ts.

1. The top-level call. export const FEATURES = FEATURE_GROUPS.flatMap(...) is a module side effect, so every importer was pinned. Now a featureList() function, called inside the render function; create.js calls it there in the generated home page too.

2. The word "window" in prose. This is the one worth knowing about. The rate-limit blurb read '...returning a 429 with Retry-After past the window.' nav.ts is neither a route module nor a component, so in component-elision.js its templateScan is masked rather than redacted, meaning CLIENT_GLOBAL_RE runs over string literals with their contents intact and matched ordinary English inside a quoted string. Reworded to "once the interval resets".

Fixing only cause 1 left the verdict unchanged, which is why the first attempt looked like a no-op.

Effect

gallery/app/page.ts: shippedinert (shippedWhole 4 → 3). It no longer ships card.ts, badge.ts, nav.ts, or the ~400-line cn.ts to every visitor.

This flows to the scaffold, since modules/ is copied into generated apps. A freshly generated app was verified end to end: webjs check clean, webjs typecheck clean, boots, home 200 with app/page.ts absent from the boot script, /features/forms 200, 25 feature cards, zero undefined holes. Its own elision verdict for app/page.ts is inert.

Repo suites after the change: test/scaffolds/* 65/65, gallery node + browser green, webjs check + typecheck clean on the gallery.

Two things found while verifying, NOT changed here

The analyser false positive outlives this reword. Any prose containing window, document, location, history etc. inside a string literal in a non-route, non-component module silently ships that module's importers. A blurb reword is a workaround, not the fix; the fix is redacting string literals before the client-global scan for such modules. That is a change to elision correctness in packages/server/src, which wants its own differential verification and Bun parity, so it is deliberately not bolted onto a merge-readiness pass.

webjs elision --verify cannot pass on the gallery, exiting 1 on /features/caching. The divergence is purely a clock: app/features/caching/page.ts:21 renders new Date().toLocaleTimeString(), and the harness renders each route twice about four seconds apart, so the timestamp differs between the elision-on and elision-off passes (reproduced twice: 17:26:33/37, then 17:26:55/59). Pre-existing, untouched by this PR, and not run by CI. The demo's live clock is the point of the demo, so the real fix belongs in the harness.

CI note

Bun runtime smoke + test matrix failed once on this commit in test/hooks/cleanup-merged-worktree.test.mjs ("hook never blocks the tool"). Unrelated to the diff (which touches only nav.ts and create.js), green on the preceding commit, and 7/7 passing three consecutive local runs under Bun. Re-ran the job: all 10 checks green. Flake.

@vivek7405
vivek7405 merged commit c60f226 into main Aug 10, 2026
19 of 20 checks passed
@vivek7405
vivek7405 deleted the feat/root-gallery-app branch August 10, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gallery): promote scaffold gallery to root gallery/ app

1 participant