Project Structure
Pulse enforces a single convention: one spec per page, one directory per concern. The structure is not a recommendation — it is how the framework discovers and registers your pages. There is nothing to configure because there is nothing to decide.
Layout
my-app/
├── package.json
├── public/ # static assets served directly
│ ├── app.css
│ └── dist/ # generated bundles (pulse build)
│ ├── manifest.json
│ ├── chunk-[hash].js # shared code, when pages share imports
│ └── [name].boot-[hash].js
└── src/
├── pages/ # one file per page — auto-discovered
│ ├── home.js
│ ├── about.js
│ └── contact.js
├── lib/ # shared helpers
│ └── db.js
└── components/ # shared view fragments (optional)
└── card.js
src/pages/
Each file in src/pages/ exports a single spec as the default export. pulse dev auto-discovers every file in this directory and registers it as a route. There is no route registry to maintain — the file is the registration.
about.js → /about). For dynamic segments, route is set explicitly in the spec.public/
Static files served directly — CSS, fonts, images. Referenced via meta.styles in the spec. The dist/ subdirectory is generated by pulse build. Its contents are content-hashed and must not be edited manually.
| Path | Purpose |
|---|---|
public/app.css | Your global stylesheet — reference it in spec meta.styles |
public/dist/ | Generated bundles from npm run build — do not edit manually |
public/dist/manifest.json | Maps spec hydrate paths to hashed bundle filenames |
src/lib/
Shared helpers used across multiple pages — database clients, API wrappers, utility functions. Imported directly into specs. Plain JavaScript modules with no framework coupling — they work identically in tests, scripts, or other contexts.
pulse dev, saving any file under src/ re-evaluates your project's modules, so edits to src/lib and src/components show up without a restart. That also re-runs module-level code: a database client, connection pool, timer or in-memory cache created at the top of a module is created again on every save and the old one is not closed. Keep long-lived resources behind globalThis.__db ??= createClient(), or in pulse.store.js. Two limits: CommonJS (.cjs) files and modules outside the project root are not refreshed, and code that parses import.meta.url by hand should use fileURLToPath (the URL gains a ?t= suffix after a reload).src/components/ (optional)
Reusable view fragments. Since views are just functions that return HTML strings, a component is simply a function:
// src/components/card.js
export function card({ title, body }) {
return `
<div class="card">
<h2>${title}</h2>
<p>${body}</p>
</div>
`
}
// src/pages/home.js
import { card } from '../components/card.js'
export default {
route: '/',
view: () => card({ title: 'Hello', body: 'Welcome to Pulse' }),
}
One file per page
Pulse enforces one spec per file. Every page is self-contained — state, view, mutations, actions, and validation in one place. This is not a style preference. It is how the framework eliminates the question of where things live.
This holds for dynamic routes too: route: '/blog/:slug' is one spec serving every matching URL — the :param does the multiplying, so a thousand blog posts are still a single file. A site is therefore a small collection of specs, one per route, sharing layouts and sections from src/components/ rather than repeating them.