create-jamstack
v0.1.0
Published
Scaffold a WebGL/WebGPU game jam starter (stack + shaders + physics + deploy).
Readme
create-jamstack
Scaffolds a project from the flags produced by the jamstack site's picker
(components/main/GeneratedCommand.tsx). Flag values are the same slugs the
site's slugify() produces from its option labels.
npx create-jamstack@latest my-jam-game --stack=r3f-vite --shaders=glsl --physics=rapier --deploy=vercel
npx create-jamstack@latest my-jam-game --stack=threejs-vite --shaders=tsl --physics=cannonjs --deploy=vercel
npx create-jamstack@latest my-jam-game --stack=pixi-vite --shaders=pixi-filters --physics=matterjs --deploy=itchioStatus
Every stack and every --shaders/--physics option in the site's picker is
now scaffoldable via the CLI — full parity with SelectionTabs.tsx, nothing
left in "known to the validator but not implemented" state.
| Stack | Engine | --shaders | --physics |
| --- | --- | --- | --- |
| r3f-vite | three | tsl, glsl, wgsl | rapier, cannonjs, ammojs, jolt |
| threejs-vite | three | tsl, glsl, wgsl | rapier, cannonjs, ammojs, jolt |
| tresjs-vue | three | tsl, glsl, wgsl | rapier, cannonjs, ammojs, jolt |
| tresjs-nuxt | three | tsl, glsl, wgsl | rapier, cannonjs, ammojs, jolt |
| pixi-vite | pixi | pixi-filters | rapier, matterjs, planckjs |
| pixi-next | pixi | pixi-filters | rapier, matterjs, planckjs |
| babylon-vite | babylon | glsl, wgsl, nme | rapier, cannonjs, ammojs, jolt |
| babylon-next | babylon | glsl, wgsl, nme | rapier, cannonjs, ammojs, jolt |
| phaser-vite | phaser | phaser-pipelines | matterjs, planckjs, arcade-physics |
--deploy is vercel, itchio, or wavedash for any of them.
Verification gap: see "Verification performed" below — the tsl/glsl
rows above (all stacks) and babylon-vite's glsl/wgsl/nme +
rapier/cannonjs/ammojs/jolt combos were scaffolded and built for
real. tresjs-vue/tresjs-nuxt's shader+physics modules, all of
phaser-vite, babylon-next with wgsl/nme, and the 3D asset
optimization loaders described below were written to the same standard
(real APIs read from shipped .d.ts/source before use — see the per-module
code comments for what was cross-checked and how) but a sandbox outage cut
the verification pass short before npm install && npm run build could be
run against them. Treat those as unverified until someone runs that command
for real.
3D asset optimization (always on, not a picker option)
Every three-engine stack (r3f-vite, threejs-vite, tresjs-vue,
tresjs-nuxt) and every babylon-engine stack (babylon-vite,
babylon-next) always gets a loaders/modelLoader.ts (path varies per
stack — see src/stacks/model-loader-{three,babylon}.js) wiring up Draco
geometry compression and KTX2/Basis texture compression, regardless of
--shaders/--physics. Unlike those two flags this isn't a stylistic
either/or choice, so it isn't a --flag or a SelectionTabs.tsx category —
every one of this CLI's 3D stacks just gets it, since real GLTF/GLB assets
in a shipped web game are the actual reason those compression formats
exist. pixi-vite/pixi-next/phaser-vite are 2D, so they're unaffected.
- three-engine stacks:
createModelLoader(renderer)returns aGLTFLoaderwith aDRACOLoaderandKTX2Loaderattached, decoding via Google's/jsDelivr's CDN-hosted decoder binaries by default (the same defaults three.js's own examples use) — see the code comment inmodel-loader-three.jsfor how to self-host those instead, and for the WebGPU-renderer caveat (detectSupportAsyncinstead ofdetectSupport). Since the demo scene is procedural and doesn't load any models, nothing calls this by default. - babylon-engine stacks: adds
@babylonjs/loadersas a dependency (the base templates only had@babylonjs/core, which has no.gltf/.glbparser at all) and aloadModel(scene, url)helper. Babylon's Draco/KTX2 decompression is built into@babylonjs/coreitself with CDN-hosted decoder defaults already configured — registering the loaders package is the only step needed to turn it on.
Two independent axes: flavor and engine
Module content depends on two things that turned out not to be the same axis, even though the first two stacks made them look that way:
flavor— the code shape:react(hooks/JSX/components —r3f-vite),vanilla-oop(plain classes, no framework —threejs-vite,pixi-vite,babylon-vite/babylon-next,phaser-vite), orvue(<script setup>SFCs —tresjs-vue/tresjs-nuxt).engine— which library the module is written against:threevs.pixivs.babylonvs.phaser.
threejs-vite and pixi-vite are both vanilla-oop but need completely
different shader/physics code, which is why modules are keyed
[flavor][engine] (src/modules/shaders/index.js /
src/modules/physics/index.js) rather than just [flavor]:
src/modules/shaders/
react/ # three engine, react flavor (r3f-vite)
vanilla/three/ # three engine, vanilla-oop flavor (threejs-vite)
vanilla/pixi/ # pixi engine, vanilla-oop flavor (pixi-vite)
vanilla/babylon/ # babylon engine, vanilla-oop flavor (babylon-vite/-next)
vanilla/phaser/ # phaser engine, vanilla-oop flavor (phaser-vite)src/modules/physics/ mirrors that layout, plus a vue/three/ directory
that shaders/index.js doesn't need — the vue flavor's shader modules
just reuse vanilla/three's directly (see the comment in
src/modules/shaders/index.js), since those modules only export a plain
THREE.Material subclass with no React/Vue coupling, so the exact same file
content works from a Vue <script setup> too. Physics modules can't be
shared that way — sceneFileContent generates a whole framework-shaped file
(a class for vanilla-oop, an SFC for vue) — so vue/three/*.js duplicates
the physics logic from vanilla/three restructured into TresJS's
useLoop()/shallowRef shape.
src/catalog.js records each stack's engine and flavor; src/validate.js
and each stack's generate() both look modules up via
MODULES_BY_FLAVOR_ENGINE[stack.flavor][stack.engine].
The vanilla-oop shape, per engine
Both vanilla-oop stacks split "fixed environment" from "generated jam
content" via a World class holding a generated GameScene
(object/view + init() + update(delta)), but otherwise lean on
whatever their engine already provides rather than reinventing it:
threejs-vite— three.js has no built-in app loop, resize handling, or camera wrapper, so this template builds a smallEventEmitterbase class plusSizes/Time(emitresize/tick) and dedicatedCamera/Rendererclasses that subscribe toSizesindependently rather thanApppushing updates into them.Appjust wires these together and relaysTime'stick→Camera.update()→World.update()→Renderer.update(). Materials areTHREE.ShaderMaterial/MeshBasicNodeMaterialsubclasses with their ownupdate(delta).pixi-vite— PixiJS'sApplicationalready provides an equivalent ofSizes/Time/Renderer(resizeTo,ticker, the renderer itself), so reimplementing those would just be duplicate abstraction;Apphere is a thin wrapper that initializesApplicationand relaysticker.add()toWorld.update(). There's noCameraclass — Pixi's 2D stage has no camera concept and this template's scene doesn't pan or zoom, so it would have nothing to do. The shader-equivalent option (pixi-filters) is aPIXI.Filtersubclass with its ownupdate(delta), applied to the demo object via.filters = [...]rather than substituted in as a material.babylon-vite/babylon-next— Babylon meshes/lights auto-register against theScenethey're constructed with, so unlike three.js/pixi'sWorldthere's no.object/.viewto parent —GameScenejust takes theScenedirectly.Appnormally builds theEngine/Scene/camera synchronously in its constructor; whenwgslis selected that all moves intoinit()instead, since aWebGPUEngineneeds an awaitedinitAsync()before anything else can touch it.phaser-vite— Phaser'sSceneclass already is the fixed environment/app-loop roleWorldplays elsewhere, soWorldhere is a thinPhaser.Scenesubclass that just constructs aGameSceneincreate()and relaysupdate(). The shader-equivalent option (phaser-pipelines) is aPostFXPipelinesubclass applied viagameObject.setPostPipeline(...); unlike every other shader module it isn't driven by an explicitupdate(delta)call — pipelines get their ownonPreRender()hook, called automatically once per frame.
Each stack's actual file layout (what's static template vs. generated)
lives in src/stacks/<stack>.js.
How it's put together
bin/create-jamstack.js— entry point: parses argv, validates the selection, calls the scaffolder.src/catalog.js— mirrors the stack/shader/physics compatibility rules fromcomponents/main/SelectionTabs.tsxon the site, so the CLI rejects combinations the site itself wouldn't let you pick.src/scaffold.js— thin dispatcher: pickssrc/stacks/<stack>.js'sgenerate()function based on--stack.src/stacks/*.js— one file per implemented stack. Copiestemplates/<stack>, then generates the files whose content depends on which shader/physics modules were selected (these can't be static files, since any shader can be paired with any physics engine).src/modules/{shaders,physics}/<flavor>/<engine>/*.js— one file per option per flavor+engine, each exporting the code it injects into the scaffolded project.src/modules/deploy/*.js— shared across every stack/flavor/engine; they only touchpackage.jsonscripts and drop config files, nothing stack-specific.src/shared.js— file-writing helpers and deploy-module wiring shared across all stacks.
Notes on the riskier modules
tslandwgsl(three engine) render through three'sWebGPURenderer— they need a browser with WebGPU support to actually run.glslruns on the default WebGL renderer.ammojsandjolt(three engine) have no maintained wrapper for either flavor, so they step the physics world by hand each frame and copy the resulting transform onto the mesh — more code thanrapier/cannonjs, which lean on maintained bindings (@react-three/rapier/@dimforge/rapier3d-compat,@react-three/cannon/cannon-es).rapierandplanckjsonpixi-viterun physics in meters and convert to pixels only when positioning graphics (PIXELS_PER_METERin each module) — both are Box2D-lineage engines tuned for roughly meter-scale bodies, and feeding them raw pixel-magnitude numbers (hundreds of units) risks an unstable simulation.matterjsskips this conversion deliberately — Matter.js's own defaults are tuned for pixel-scale usage.pixi-filtersforces the wholepixi-viteapp to the WebGL backend (preference: "webgl"incore/App.ts) because it only ships aglProgram, not thegpuProgramPixi's WebGPU backend needs — without forcing WebGL, the filter would silently no-op on a WebGPU-preferring browser (perFilter's own doc comment inpixi.js).wavedash's deploy module doesn't shell out to a Wavedash-specific CLI command — that flow wasn't something this template could verify — so it just documents the build output (dist/) for you to upload by hand.nme(babylon) hand-builds aNodeMaterialblock graph via code (InputBlock/TransformBlock/TrigonometryBlock/LerpBlock/etc.) rather than loading one from the visual Node Material Editor or its snippet server, neither of which is available offline.NodeMaterialcompiles to whichever backend (EngineorWebGPUEngine) is active, so unlikewgslit doesn't force a WebGPU engine.wgsl(babylon) uses Babylon's own WGSL preprocessor dialect (ShaderStore.ShadersStoreWGSL,attribute/varying/uniformdeclarations,vertexOutputs/fragmentOutputsstructs) rather than raw WGSL — mirrored from the shaders shipped under@babylonjs/core/ShadersWGSL/*.vertex.js/*.fragment.js, since that convention isn't obvious from the public API surface alone.matterjs/arcade-physicsonphaser-viteuse Phaser's own built-in Matter/Arcade physics plugins (configured via thePhaser.Gameconfig'sphysicsblock) rather than driving a physics world by hand — Phaser auto-syncs each Game Object's transform with its body every frame, unlike every other engine's physics modules in this CLI.planckjshas no built-in Phaser integration, so it's manually driven like the pixi-vite version.tresjs-vue/tresjs-nuxt'stsl/wgslshaders need a WebGPU renderer;TresCanvas'srendererprop takes a synchronous factory, but TresJS internally detects a WebGPU-styleRenderer("isRenderer" in value) and awaits its.init()before starting the render loop — confirmed by reading@tresjs/core's compiled source, since this isn't documented as prominently as React Three Fiber's equivalent (gl={async (props) => ...}) that it mirrors.
Verification performed
Every --shaders / --physics / --deploy value on r3f-vite,
threejs-vite, and pixi-vite, plus babylon-vite/babylon-next with
glsl across all four physics options, was scaffolded and run through npm
install && npm run build against the real currently-published versions of
every dependency. This caught and fixed real bugs each time it was done:
r3f-vite: a missingvite-env.d.ts, two TSL node type-cast errors.threejs-vite: a wrong relative import path (GameScene.tslives one directory deeper thanScene.tsxdid), an unused-private-field error in theammojsmodule.pixi-vite: none on the first real build — the Pixi v8 filter API (Filter/GlProgram/defaultFilterVert) and physics package APIs (@dimforge/rapier2d-compat,matter-js,planck) were read directly out of their shipped.d.ts/source before being used, rather than written from memory first, specifically to avoid the trial-and-error cycle the other two stacks needed.matter-js'sexport =default export did require addingesModuleInterop: trueto the template'stsconfig.jsonup front.babylon-vite/babylon-next(glsl+rapier/cannonjs/ammojs/jolt): none — built clean on the first try.
The Jolt integration's WASM API calls were additionally cross-checked by
hand against the .d.ts shipped in jolt-physics.
babylon-vite with wgsl and with nme (each paired with a physics
option) were also scaffolded and built for real, with no errors — this
confirms the WGSL preprocessor dialect and the hand-built NodeMaterial
graph are both syntactically and type valid.
What wasn't verified, because a sandbox outage (Bash became
unavailable mid-session, coinciding with an npm install filesystem error)
cut this verification pass short:
babylon-nextwithwgsl/nme(theWebGPUEngine-specificApp.tsvariant,buildBabylonWebGPUAppTs({ variant: "next" }), was never actually built).- All of
tresjs-vue/tresjs-nuxt's new shader and physics modules (src/modules/physics/vue/three/*.js,src/stacks/tresjs-shared.js). - All of
phaser-vite's new shader and physics modules (src/modules/{shaders,physics}/vanilla/phaser/*.js).
Those were written to the same standard as everything else here — real APIs
read from shipped .d.ts/source before use (see the code comments in each
module for specifics) — but that's not a substitute for an actual npm
install && npm run build, let alone a browser. Run that command against
them before relying on this table.
Separately, and true of every stack including the previously-verified ones: nothing here was ever run in an actual browser, so in-scene correctness (physics behavior, WebGPU rendering, shader/filter/pipeline visuals) is unconfirmed beyond "it type-checks and bundles" even where the build itself was verified.
