Site overhaul, Tetris tutorial chapter, and library catch-up from fixes#57
Open
bigmistqke wants to merge 159 commits into
Open
Site overhaul, Tetris tutorial chapter, and library catch-up from fixes#57bigmistqke wants to merge 159 commits into
fixes#57bigmistqke wants to merge 159 commits into
Conversation
`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.
# Conflicts: # README.md # pnpm-lock.yaml
…s remount to change
…ns for supply-chain policy
…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.
commit: |
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 recentfixesmerge — gl-prop flat refactor, browser-mode testing migration,createThreecanvas 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/replthat compiles the snippet withbabel-preset-solidand runs it in an iframe; the reader can edit the source in-place. Two top-level sections:Tutorial (nine chapters)
Canvas,<T.Mesh>, the smart-prop pipeline.args, reactive props.<Show>,<For>,<Index>,<Switch>/<Match>,<Suspense>.stopPropagation,onClickMissed.useLoader,<Resource>, loader cache.<T.Mesh>."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-styleglprop documented, with an explicit note that constructor params are applied once at first construction and are never re-read (becausecanvas.getContextis idempotent — to swap, unmount and remount<Canvas>). Defaults table coversframeloop,gl,shadows, tone-mapping, color-space.useProps— rewritten to match the actual coercion order insrc/props.ts.Entity/createEntity— moved off theTpage where it didn't belong.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>componentThe piece that makes the whole site tick. Wraps
@bigmistqke/repl, pre-bundles the localsolid-threefor the iframe, gates the editor open on click, syncs the editor theme with the site's light/dark mode, syntax-highlights viatm-textarea, surfaces compile errors without killing the iframe, and bakescolor-schemeinto the iframe HTML at creation so there's no white flash in dark mode.Renderer type default (small library follow-up)
The
fixesmerge leftResolvedRendererdefaulting to the openRendererunion, which forced every project to declare aRegisteraugmentation just to callWebGLRenderer-specific methods onuseThree().gl. This PR flips the default:ResolvedRendererdefaults toWebGLRenderer— the common case is friction-free; users on other renderers opt in viaRegister.Register/Renderer/ResolvedRendererre-exported at the top level sodeclare module "solid-three" { interface Register { ... } }works directly (previously the symbols were only accessible via theS3namespace, so augmentation didn't merge).solid-three.d.tssince the site demos both WebGL and WebGPU renderers.tests/setup.tsso mockRendererLikerenderers in test fixtures keep type-checking.This is the only library change in this PR; everything else under
src/was already infixes.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.tsxbabel-preset,vite.config.tsextensionAlias 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.