A project is a folder: a real npm package whose entry file (package.json main, index.tsx by default) default-exports a Solid component rendering a <stage>.
export default function Project() {
return (
<stage background="#161616" camera={[0.3, 0, 0, 0.3, 85, 150]}>
<scene name="Intro" width={1920} height={1080} fill="black" active>
{/* ... */}
</scene>
</stage>
);
}The component receives no props. dapi open <dir> opens the folder — creating it, and an index.tsx holding an empty stage, if it is not a project yet.
open writes as little as it takes to make a folder a project, and the rest of the scaffold appears lazily. What ends up there:
| Path | What it is |
|---|---|
index.tsx |
The entry. Its default export renders the composition. |
package.json |
The project record: projectId (its identity, kept across renames), displayName, main, and the dapi commands as scripts. |
tsconfig.json |
Types for the composition tags, through jsxImportSource. |
assets.yml |
The asset library (see media.md). |
assets/ |
The library's files: symlinks to media brought in from elsewhere, plus what the app produced itself, generations under assets/generated/. Media imported through the app is linked where it lies, never copied. |
cache/ |
Derived data (thumbnails, waveforms). Disposable. |
AGENTS.md |
The agent entry point: what to read in .diffusion/docs/. |
.diffusion/docs/ |
App-owned copy of this reference and the examples, stamped with the app version and regenerated when it changes. Read it, never edit it. |
Everything but .diffusion/ is written once and is yours from then on.
Every composition element carries an id, and the app stamps one onto every element that lacks it before each compile — written back into your source, six base-36 characters, unique within its file:
<rect id="k3f9x1" x={40} y={40} width={640} height={360} fill="#FF0055" />An id is what makes an element addressable: it is how an entity is traced back to the JSX that produced it, so a change made on the canvas can be written to the element it came from, and it is what dapi capture takes to render one node. It is also how elements point at each other within a render — syncTo names the id of the clip it aligns against.
Ids are yours to write. Any string is valid, and id="hero" is worth more to read than anything minted. The stamp only fills in elements that have none, so renaming one by hand is safe. Ids are stripped at compile time and never reach the runtime as a prop; what survives is the stamp that carries them.
Imports resolve by category:
- Host modules (marked external at compile time, resolved in-app so the project shares the editor's reactive runtime):
solid-js,solid-js/store,@diffusionstudio/jsx. These must be the editor's own instance and never come from anywhere else — a project's ownnode_modulescopy is types only. - Userland packages — any other bare specifier (
three,gsap,d3-scale, …). A project folder is a real npm package, so these work as they normally do: install one (npm i three) and esbuild resolves it from the project'snode_modulesand bundles it into the compiled module, subpath imports andexportsmaps included. Nothing is installed for you and there is no CDN fallback — a specifier that does not resolve fails the compile withCould not resolve "three", and the canvas keeps the last good render. Libraries must be browser-compatible (no Node builtins); sources undernode_modulesskip the JSX transform, so a package must ship compiled JavaScript rather than raw JSX. - Local imports: relative/absolute paths (
./helper, local JSON) are resolved on disk and bundled. Statichttps://…imports are not supported — they survive bundling as a require the renderer cannot satisfy, and fail at mount. - The module executes inside the editor process, unsandboxed. This is local tooling with a local trust model, the same trust as running the app itself. Only effects made through the JSX runtime are part of the document; anything else the module does is unsupported.
- Solid's control flow (
<For>,<Show>,<Index>,<Switch>) and primitives (createSignal,createMemo, …) are fully available. Control-flow components need their import (import { For } from "solid-js"); the compile says so by name when one is missing. See lifecycle.md for what happens after mount.
Composition elements are camelCase intrinsics (<rect>, <keyframeTrack>, <linearGradientPaint>); the compile canonicalizes them to the components the renderer receives. Writing one PascalCase (<Rect>) is a compile error naming the tag you meant.
Lowercase DOM tags are the vocabulary for <html> content. Three names belong to both vocabularies — rect, text, image — and resolve lexically: inside an SVG container (<svg>, <g>, <defs>, …) they are SVG content, everywhere else composition elements.
@diffusionstudio/jsx ships the JSX namespace (which types the camelCase composition tags), for editor IntelliSense and typechecking in a project folder:
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "@diffusionstudio/jsx"
}
}The compile does not typecheck; types are stripped. Run npx tsc --noEmit in the project folder for type safety.
Installing a userland package gives it both its runtime code and its types. A package that ships no declarations of its own needs its @types/… alongside it (npm i -D @types/three). @diffusionstudio/jsx and solid-js are the exception: they are declared in the scaffolded package.json for types only, since the compiler always keeps them external and the running composition uses the app's own instance.