Skip to content

Site overhaul, Tetris tutorial chapter, and library catch-up from fixes#57

Open
bigmistqke wants to merge 159 commits into
solidjs-community:nextfrom
bigmistqke:next-site
Open

Site overhaul, Tetris tutorial chapter, and library catch-up from fixes#57
bigmistqke wants to merge 159 commits into
solidjs-community:nextfrom
bigmistqke:next-site

Conversation

@bigmistqke
Copy link
Copy Markdown
Contributor

@bigmistqke bigmistqke commented May 27, 2026

What's in this PR

A new docs site for solid-three (site/, ~21k lines, 93 files) plus a small follow-up refactor of the renderer-type default. The library/test-infrastructure work that came in via the recent fixes merge — gl-prop flat refactor, browser-mode testing migration, createThree canvas swap, etc. — is not described below; it's its own concern and reviewable separately.

The docs site

Scaffolded under site/ with SolidStart and SolidBase, rendered with MDX. Every code sample is a live <Demo> — a small wrapper around @bigmistqke/repl that compiles the snippet with babel-preset-solid and runs it in an iframe; the reader can edit the source in-place. Two top-level sections:

Tutorial (nine chapters)

  1. Your first sceneCanvas, <T.Mesh>, the smart-prop pipeline.
  2. Props and children — nesting, transforms, args, reactive props.
  3. Control flow<Show>, <For>, <Index>, <Switch>/<Match>, <Suspense>.
  4. Pointer events — click, hover, stopPropagation, onClickMissed.
  5. useFrame — signal form, imperative form, priority ordering.
  6. Loaders & ResourceuseLoader, <Resource>, loader cache.
  7. Portal — escape transforms; render-to-texture.
  8. Tetris — built across three demos (render → gravity+keys → lines+score). The chapter's punchline: "game development is state management; Solid is a state manager that happens to render <T.Mesh>."
  9. A peek at WebGPU — WebGPURenderer factory, TSL basics, a TSL uniform-driven slider.

API reference

Ported and rewritten from the README into per-export pages, organised by component / hook / event / utility (16 pages). Notable rewrites:

  • Canvas — flat r3f-style gl prop documented, with an explicit note that constructor params are applied once at first construction and are never re-read (because canvas.getContext is idempotent — to swap, unmount and remount <Canvas>). Defaults table covers frameloop, gl, shadows, tone-mapping, color-space.
  • useProps — rewritten to match the actual coercion order in src/props.ts.
  • Entity / createEntity — moved off the T page where it didn't belong.
  • Events overview — consolidated event-object shape, intersection fields, smart-prop pipeline.

Home page

A 3D hero scene of SOLID THREE letters dropped with cannon-es physics, cursor repulsion, click-to-punch, camera drift, fake contact shadows, studio reflection. A toggle lazy-mounts a <Demo> editor over the scene so the home page doubles as a REPL into its own source.

<Demo> component

The piece that makes the whole site tick. Wraps @bigmistqke/repl, pre-bundles the local solid-three for the iframe, gates the editor open on click, syncs the editor theme with the site's light/dark mode, syntax-highlights via tm-textarea, surfaces compile errors without killing the iframe, and bakes color-scheme into the iframe HTML at creation so there's no white flash in dark mode.

Renderer type default (small library follow-up)

The fixes merge left ResolvedRenderer defaulting to the open Renderer union, which forced every project to declare a Register augmentation just to call WebGLRenderer-specific methods on useThree().gl. This PR flips the default:

  • ResolvedRenderer defaults to WebGLRenderer — the common case is friction-free; users on other renderers opt in via Register.
  • Register / Renderer / ResolvedRenderer re-exported at the top level so declare module "solid-three" { interface Register { ... } } works directly (previously the symbols were only accessible via the S3 namespace, so augmentation didn't merge).
  • Site widens back to the open union via a local solid-three.d.ts since the site demos both WebGL and WebGPU renderers.
  • Tests widen back in tests/setup.ts so mock RendererLike renderers in test fixtures keep type-checking.

This is the only library change in this PR; everything else under src/ was already in fixes.

Process / docs

Specs and implementation plans for the bigger pieces of work (tutorial structure, hero splash REPL, tetris chapter) live under docs/superpowers/{specs,plans}/.

Test plan

  • pnpm test — 9 files, 151 passed, 2 todo, 0 failures.
  • pnpm exec tsc --noEmit — clean apart from two pre-existing, unrelated errors (demo.tsx babel-preset, vite.config.ts extensionAlias drift).
  • pnpm build — both ESM and DTS build successfully.
  • cd site && pnpm dev — walk the tutorial chapters end to end, exercise the API reference pages, confirm the hero scene plays.

bigmistqke added 30 commits May 26, 2026 17:56
`useLoader` resources read into a `<T.* />` prop inside `<Suspense>` (without
an explicit `fallback`) used to throw "S3: Hooks can only be used within the
Canvas component!" once the resource resolved. Reproduces with the user's
minimal pattern:

```tsx
function TexturedCube() {
  const texture = useLoader(THREE.TextureLoader, "…")
  return <T.Mesh><T.MeshBasicMaterial map={texture()} /></T.Mesh>
}
<Canvas><Suspense><TexturedCube /></Suspense></Canvas>
```

## What was happening

createThree wrapped the user's children in `mergeProps`:

```ts
useSceneGraph(context.scene, mergeProps(props, {
  get children() { return c() }
}))
```

`mergeProps` builds a `sources = [overrideGetter, canvasPropsGetter]` chain
for the `children` key and resolves via `resolveSources`, which returns the
first NON-undefined source value. When `c()` returned `undefined` (which
happened transiently while the reactive chain re-evaluated through a
nested `<Suspense>`), `resolveSources` fell through to the user's raw
`<Canvas>` JSX `get children()` accessor and invoked it — spawning a fresh
`Suspense` component from scratch in an owner that didn't include the
canvas's context providers. That second Suspense's `TexturedCube` then
ran `useThree()` and threw.

## The fix

`useSceneGraph` only reads `children` (and an optional `onUpdate` Canvas
never passes). There's no reason to merge with the full Canvas props
surface. Pass a clean object with just the override; no fallback chain to
the original `<Canvas>` children getter.

## Test

Adds tests/core/use-loader-suspense.test.tsx covering the exact pattern.
Verified the test fails on HEAD~1 and passes after the fix.
The three useLoader hook tests were commented out — they exercise patterns
that the recently-fixed Suspense+useSceneGraph interaction (see prior
commit) now handles correctly. Re-enabling them lands real regression
coverage for the user-facing pattern:

  function Component() {
    const resource = useLoader(...)
    return <Show when={resource()}>{r => <Entity from={r()} /></Show>
  }
  <Suspense fallback={null}><Component /></Suspense>

Adjustments to make them pass:
- `Primitive` doesn't exist in solid-three — use `Entity from={...}`.
- Loader-extension callback is now passed as `{ onBeforeLoad }` on the
  options object, not as a third positional argument.
- The "array of URLs" test was rewritten to use a Record. solid-three's
  `LoadInput` is `LoaderUrl | Record<string, LoaderUrl>` — array-of-URLs
  isn't supported (the test was carried over from a different API). The
  Record variant exercises the same multi-load path through
  `awaitMapObject`.
…fter resolve

The original regression test only verified no "Hooks can only be used"
error fired and that the component was instantiated exactly once. Beefs
the file up with:

1. The original no-fallback test now also captures the canvas's scene via
   `useThree()`, asserts no meshes are present while the resource is
   pending, and asserts exactly one mesh is present after resolve.

2. New test for explicit-fallback Suspense: asserts a named `fallback`
   mesh is in the scene while pending and gone after resolve, with the
   named `content` mesh swapping in. Verifies the full Suspense
   fallback↔content swap through solid-three's scene graph.
Resolves the f4d310b failing repro. `applySceneGraph` was using
`instanceof Material`, `instanceof Object3D`, etc. to decide auto-attach
slots. That fails when a class comes from a different module instance of
three — most notably `MeshBasicNodeMaterial` from `three/webgpu` won't
share class identity with the `Material` base imported from `three`.

Three.js itself uses duck-typed `is*` markers (`isMaterial`,
`isObject3D`, `isBufferGeometry`, `isFog`) as the cross-version contract.
Mirror that here.

- Adds `isMaterial`, `isObject3D`, `isBufferGeometry`, `isFog` helpers in
  utils.ts (alongside `isWebXRManager` / `isWebGLShadowMap`).
- Replaces `instanceof Material/Object3D/BufferGeometry/Fog` in
  `src/props.ts` with the new helpers. The three.js imports in props.ts
  become type-only since nothing else needs them at runtime.
- The pre-existing failing test "attaches a foreign Material (duck-typed
  isMaterial: true)" now passes.
The previous async-createEffect-with-closure-flag pattern for awaiting
`WebGPURenderer.init()` had several rough edges:
- Mutated a closure `glInitialized` boolean from inside an async effect.
- Hand-rolled cancellation via a `let cancelled` flag + onCleanup.
- No built-in error path if `init()` rejected.

`createResource` is the idiomatic primitive for "tracked async load":
- Source = `() => gl()`. When the renderer swaps, the in-flight fetch is
  cancelled automatically — no manual `cancelled` flag.
- Fetcher returns synchronous `true` for renderers without `init()`
  (resource is `"ready"` immediately, render loop can render) or a
  Promise that resolves once `renderer.init()` finishes (resource is
  `"pending"` until then).
- `render()` gates on `rendererReady.state !== "ready"` instead of a
  closure boolean.

Canvas pre-sizing (workaround for WebGPU's depth-buffer dimension lock)
is unchanged — still happens before awaiting init.

No behavior change observable to tests; all 10 init-touching tests pass.
Resource exposed an `attach` prop in its types and docs, but it was
stripped by useProps' splitProps and never written to the resource's
meta. When the resource was rendered as a JSX child, the parent's
applySceneGraph fell through to the auto-attach fallbacks
(material/geometry/fog/object3d) and ignored `attach` entirely — so
the documented `<Resource ... attach="map" />` pattern silently
attached nothing (or errored for non-Object3D resources).

Wrap the resolved value with meta(..., { props }) so the surrounding
scene graph sees attach (and any other meta-driven props) on the
loaded resource.
Wires @mdx-js/rollup with solid-mdx provider into the tutorial Vite config,
adds remark-frontmatter/remark-mdx-frontmatter plugins, and renders a smoke-test
chapter (00-hello.mdx) that exports a typed frontmatter object.
Replace the bespoke Vite + custom-MDX scaffolding with a SolidStart +
@kobalte/solidbase site. Salvages the @bigmistqke/repl-backed `<Demo>`
component and the local solid-three esbuild bundle plugin verbatim.

The tutorial is now its own isolated project (no longer driven by the
root vite config): tutorial/package.json with @solidjs/start v2 alpha,
@kobalte/solidbase 0.6.x, vite 8, nitro. Root `dev:tutorial` /
`build:tutorial` cd into tutorial/ and run `vite dev` / `vite build`.

Chapters are MDX files under `src/routes/`, the sidebar (Parts I-VI) is
configured in `vite.config.ts`, and Demo is exposed globally to MDX via
`src/theme/mdx-components.tsx` (SolidBase's componentsPath convention).

A small fallback plugin works around a packaging quirk in SolidBase 0.6.3
where some `.jsx` files in dist are referenced via `.js` extensions.
Demo is rendered as an empty placeholder during SSR because
@bigmistqke/repl depends on DOMParser; the client takes over on hydration.
…stqke/repl

The previous pipeline ran ts.transpile twice (preserve -> ReactJSX with
jsxImportSource: 'solid-js'), producing _jsx(...) calls that import from
solid-js/jsx-runtime. That module doesn't export 'jsx' (Solid compiles
JSX to _$template/_$insert calls, not a runtime jsx() factory), so
snippets failed at load time.

Use @bigmistqke/repl's babelTransform to load @babel/standalone +
babel-preset-solid from esm.sh at runtime, and run Solid's real JSX
transform on the snippet. TS syntax is stripped first via ts.transpile
(JsxEmit.Preserve) so Babel only handles JSX -- this also sidesteps the
@babel/preset-typescript filename requirement that babelTransform
doesn't forward.

The Babel bundle resolves asynchronously, so the Extension transform
returns an Accessor that swaps in the compiled output once the CDN
imports settle; before then it emits a placeholder default export so
the iframe loads without throwing.
bigmistqke added 17 commits May 27, 2026 01:32
# Conflicts:
#	README.md
#	pnpm-lock.yaml
…f open Renderer union

Register augmentation is now opt-in: declare module 'solid-three' { interface Register { renderer: WebGPURenderer } } to swap, or { renderer: Renderer } to widen back. Re-exports Register/Renderer/ResolvedRenderer at top level so they can be augmented directly. Site widens for mixed-renderer demos; library tests widen for RendererLike mocks. WebGL-only call sites in 07-portal-render-target and hero cast to WebGLRenderer at the boundary.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 27, 2026

commit: ab61599

bigmistqke added 12 commits May 27, 2026 02:30
… solidBaseJsxFallbackPlugin covers the only use case)
…e SSG

The previous stack (SolidStart 2 PR build + SolidBase 0.6.3 + Vite 8 +
Nitro 3 beta) failed to build with a Nitro SSR-entry mismatch, and the
published Start 2 alpha is incompatible with Vite 8. Move back to the
last well-trodden line: vinxi + Start 1.x + SolidBase 0.2.20 + Vite 6.

- Replace site/vite.config.ts with vinxi-based site/app.config.ts using
  createWithSolidBase(...)(startCfg, sbCfg). SSR + crawlLinks prerender
  is on, producing 27 static HTML files in .output/public/.
- solidThreeBundlePlugin: resolve the library entry from process.cwd()
  instead of import.meta.url — vinxi bundles the plugin and the URL no
  longer points at the source tree.
- Demo component: wrap the return in <NoHydration>. The SSR placeholder
  and the client-rendered repl have different DOM shapes, which would
  otherwise trip a hydration mismatch on every demo page.
- Inject a __filename/__dirname ESM banner in the nitro prerender
  bundle. @kobalte/solidbase/dist/config/mdx.js imports typescript
  (CJS) at module top level, which crashes Nitro's ESM prerender
  without the shim.
- .gitignore: also ignore site/.vinxi/ build artefacts.
CI's pkg.pr.new workflow runs under corepack@latest (pnpm 11.3.0), and
pnpm 11.1.3+ re-validates pnpm-lock.yaml against the minimumReleaseAge
policy at install time. The lockfile from the previous commit contained
ioredis@5.11.0 and streamx@2.26.0, both published less than 24h before
the CI run, so install aborted with
ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION.

- Re-resolve pnpm-lock.yaml under pnpm 11.3.0 so transitive deps fall
  back to policy-compliant versions (ioredis 5.10.1, streamx 2.25.0).
- Fix pnpm-workspace.yaml: the @parcel/watcher entry under allowBuilds
  held a literal placeholder string instead of `true`, so pnpm 11
  refused to run its native-binding postinstall.
- Pin packageManager to pnpm@11.3.0 and add engines.pnpm >=11.3.0 so
  every contributor installs under the same policy regime.
…o v2

True SSG of all 27 routes to .output/public/, ready to serve from gh-pages.
Reverses the 1a5b0b3 downgrade now that a workable modern stack exists.

Recipe (proven end-to-end in a sanity-check repo first):

- vinxi -> raw Vite via site/vite.config.ts.
- @solidjs/start pinned to pkg.pr.new/...@2080 (the SolidStart 2 PR build
  with Vite 8 + Rolldown support); forced via workspace `overrides` so the
  Vite-7-peer npm alpha doesn't compete. Same recipe solidjs/solid-docs runs.
- Nitro via @solidjs/vite-plugin-nitro-2 (pkg.pr.new@2080), which wraps the
  stable Nitro v2 prerender. Nitro v3's prerender is still beta and not
  wired up to SolidStart 2 yet - that's what broke our last attempt.
- site/vite-plugins/solidbase-jsx-fallback.ts: patches @kobalte/solidbase's
  published dist, which ships .jsx files whose imports point at .js (relies
  on the .js -> .jsx fallback Vite 8 + Rolldown no longer does). Filed
  upstream: kobaltedev/solidbase#142.

Other changes the migration forced:

- src/components/demo.tsx no longer statically imports `typescript`. TS
  bootstraps `getNodeSystem()` at module load, which crashes Nitro's ESM
  prerender on the missing CJS __filename. Replaced with a dynamic
  https://esm.sh/typescript@5.9 import gated behind first use.
- src/theme/mdx-components.tsx wraps Demo in clientOnly so the module
  (which still pulls @bigmistqke/repl, tm-textarea, the TS CDN load) never
  loads on the server. The previous NoHydration + SSR-placeholder pattern
  isn't enough - the module itself has to not be imported during SSR.
- Drop typescript and @babel/plugin-syntax-import-attributes deps (no
  longer needed once TS is CDN-loaded and we're off vinxi).
- pnpm-workspace.yaml: catalog `@kobalte/solidbase ^0.6.3`, override
  @solidjs/start to pkg.pr.new@2080 and @solidjs/router to ^0.16.1.
- Letters spawn KINEMATIC and rotate around -X/2 so the geometry's
  natural up-axis points along world Y; a staggered setTimeout flips
  each to DYNAMIC for a sequenced drop.
- Replace cursor-driven force field with a simpler click-impulse,
  applied at a small random offset for varied spin.
- Lay letters out in two rows along Z (front/back) instead of a single
  X line; row spacing and Z offset are named constants.
- Tighten initial orientation jitter and damping so resting state is
  stable; lower click impulse magnitudes to match.
- Drop the now-unused pointer raycaster + cursor tracking.

Also drop the explicit background on .demo-reset so it inherits the
surrounding pane colour instead of locking to the SolidBase variable.
`Demo` is loaded via `clientOnly` at the import site (mdx-components.tsx),
so the module never runs server-side. The previous SSR-placeholder + NoHydration wrapper was both unnecessary and broken — `NoHydration` and `isServer` were no longer imported, so the dev build threw `ReferenceError: NoHydration is not defined` and blanked every tutorial page.
Removed in the migration on the theory that it was unused, but the
`import type ts from "typescript"` type-narrowing in demo.tsx still needs
the package present at typecheck time. CI broke with `TS2307: Cannot find
module 'https://esm.sh/typescript@5.9'`. Runtime still loads TS from the
ESM CDN — this only puts the type defs back on the disk.

Also factor the CDN url into a local variable so the dynamic-import call
site reads more clearly.
…zation

Inline style attribute now serialized as 'width:100%;height:100%' (no
spaces, no trailing semicolon) instead of 'width: 100%; height: 100%;'.
Cosmetic — both are valid CSS — but the snapshot needs to match.
vite-plugin-solid defaults `test.environment` to 'jsdom' when the user
doesn't set one. That then makes vitest exit code 1 with
"MISSING DEPENDENCY Cannot find dependency 'jsdom'" — even though we
run tests in real-browser mode via @vitest/browser-playwright and don't
need jsdom at all. Tests still pass, so the failure was invisible apart
from the non-zero exit, but it red-lit CI.

Setting `environment: 'node'` skips the Solid plugin's default branch
(`if (!userTest.environment) test.environment = 'jsdom'`); real DOM
work continues to happen in Chromium via the `browser` block below.
Two passes informed by background-agent audits:

1. Tone / jargon cleanup
   - Drop "smart-prop" terminology from every page that mentioned it
     (tutorial chapters 5, 8, 9 and the useProps / Entity pages).
   - Replace React-vocabulary leaks: Solid does not "re-render",
     components run once and only the reactive bindings re-run.
   - Strip marketing-grade phrasing: "drop-in", "just works", "the most
     powerful", "stays out of the way", "carries across cleanly",
     "No GLSL, no shader cache, no uniform plumbing" — all rewritten to
     be concrete about what actually happens.
   - Replace hand-wavy "Solid takes care of it" patterns with the
     setter ladder (`.set` / `.setScalar` / `.copy`) and a pointer to
     the full coercion table on useProps.
   - Gloss three.js terms on first appearance: TSL, GLSL, InstancedMesh,
     uv, render-to-texture.

2. Interlinking
   - Define what an Entity is at the top of entity.mdx; link `Entity`
     and `T` from every page that mentions them (canvas, autodispose,
     use-props, events overview, raycasters, testing, tutorial 3/4/5/
     7/8/9).
   - Add API back-links from utilities and tutorial chapters to their
     concept pages: useFrame, useThree, Portal, Canvas, raycasters.
   - Fix raycastable.mdx (zero outbound links → linked to events
     overview) and expand testing.mdx (was 3 thin bullets → full
     test / cleanup / TestCanvas writeup).
   - autodispose.mdx now actually defines what autodispose does.

3. Code fix
   - entity.mdx: `new MeshBasicMaterial("orange")` was invalid; the
     constructor takes a parameters object.

Sidebar: rename "Let's make Tetris" → "Let's build Tetris!" to match
the capstone framing of chapter 8.

Filed upstream while writing this: solidbase publishes .jsx files
whose `.js`-string imports rely on Vite's `.js → .jsx` fallback, which
Vite 8 + Rolldown drops — see kobaltedev/solidbase#142. Our local
`solidbaseJsxFallback` plugin still patches it.
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.

1 participant