GitHub

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.

Routes are derived from filenames by default (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.

PathPurpose
public/app.cssYour global stylesheet — reference it in spec meta.styles
public/dist/Generated bundles from npm run build — do not edit manually
public/dist/manifest.jsonMaps 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.

In 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
// src/components/card.js
export function card({ title, body }) {
  return `
    <div class="card">
      <h2>${title}</h2>
      <p>${body}</p>
    </div>
  `
}
src/pages/home.js
// 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.

For large pages, view fragments and helpers can be imported from other modules. The spec file remains the coordination point — the structure is always clear regardless of how the implementation is organised.