Project Structure Conventions
npm create scenerystack@latest scaffolds a project, but it doesn't force a folder layout on your simulation-specific code — the conventions below are the ones PhET's own simulations (and most SceneryStack projects) converge on, because they keep model-view separation visible in the filesystem, not just in the code.
A typical layout
my-simulation/
├── js/
│ ├── my-simulation-main.ts # entry point: builds Screens and the Sim
│ ├── my-screen/
│ │ ├── model/
│ │ │ └── MyScreenModel.ts
│ │ └── view/
│ │ ├── MyScreenView.ts
│ │ └── MyThingNode.ts
│ └── common/ # code shared by more than one screen
│ ├── model/
│ └── view/
├── images/ # source art (SVG/PNG), one file per asset
├── sounds/ # audio cues, if the sim uses scenery-phet sound
├── my-simulation-strings_en.json # translatable strings, see Translation and Localization
├── package.json
├── tsconfig.json
└── index.htmlThe top-level split that matters most: one folder per screen, each with its own model/ and view/ subfolders, plus a common/ folder for anything shared across screens. This mirrors the Screen/ScreenView pairing described in Your First Simulation — if your sim only has one screen, the per-screen folder is still worth keeping separate from common/ so a second screen has somewhere obvious to go later.
Where things live
| Path | Contents |
|---|---|
js/<screen-name>/model/ | Plain TypeScript classes holding Property/Emitter state — no scenery imports |
js/<screen-name>/view/ | ScreenView subclass plus any Node subclasses specific to that screen |
js/common/ | Model or view code shared by two or more screens |
images/ | Source images referenced via generated image modules (PNG, SVG, JPG) |
sounds/ | Audio files referenced the same way, for scenery-phet/tambo sound generators |
<sim-name>-strings_en.json | The English source-of-truth strings file — see Translation and Localization |
<sim-name>-main.ts | The only file that imports Sim/Screen and calls onReadyToLaunch |
Keep model/ free of scenery imports
The fastest way to lose model-view separation over time is a stray import { Node } from 'scenerystack/scenery' inside a model file "just this once." If a model file ever needs to import from scenerystack/scenery, sun, or scenery-phet, that's a signal the code belongs in view/ instead.
Where to go next
- Running and Building a Simulation — what the dev server and production build do with this layout
- Model-View Separation — the architectural rule this layout exists to reinforce
- Translation and Localization — how the strings file at the repo root becomes runtime
StringPropertyinstances