npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noego/forge

v0.1.24

Published

[![NPM Version](https://img.shields.io/npm/v/@noego/forge.svg)](https://www.npmjs.com/package/@noego/forge) [![License](https://img.shields.io/npm/l/@noego/forge.svg)](LICENSE)

Readme

@noego/forge

NPM Version License

OpenAPI-Powered Svelte SSR Framework - Build server-rendered Svelte applications with routes generated directly from your OpenAPI specification.

Overview

Forge is a framework that generates Svelte applications from OpenAPI definitions, combining server-side rendering (SSR) with client-side navigation.
You get fast first paint & SEO (SSR) and instant page transitions (client-side routing) without writing a single line of glue code.

📝 Just want to see it run? Jump to the TL;DR section below and paste the two commands – you’ll have a working demo at http://localhost:3000.

Key Features

  • OpenAPI-Driven Development: Define routes, views, and layouts directly in your OpenAPI spec
  • Server-Side Rendering: Deliver fully rendered HTML on first load for SEO and performance
  • Client-Side Navigation: Smooth, fast transitions after initial load
  • Nested Layouts: Create complex page structures with multiple layout levels
  • Automatic Parameter Parsing: Route parameters (like /users/:id) are parsed from OpenAPI paths
  • Data Loading: Unified mechanism for data fetching in SSR and client-side
  • Vite Integration: Modern, fast development with hot module reloading

Getting Started

Prerequisites

Forge requires:

  • Node.js: 16.x or higher
  • Express: ^5.1.0 (peer dependency)
  • Svelte: ^5.28.2 (peer dependency)

For development:

  • Vite: ^6.3.5 (recommended for hot-module reloading and builds)
  • tsx: ^4.19.3 (recommended for running TypeScript entry files)

The versions shown in the badge at the very top are the ones Forge is tested against. Newer minor/patch releases of Express and Svelte usually work fine as well.

Installation

Install Forge and its peer dependencies:

# npm
npm install @noego/forge express@^5 svelte@^5

# yarn
yarn add @noego/forge express@^5 svelte@^5

# pnpm
pnpm add @noego/forge express@^5 svelte@^5

Then add development dependencies:

# npm
npm install --save-dev vite@^6 tsx @sveltejs/vite-plugin-svelte

# yarn
yarn add --dev vite@^6 tsx @sveltejs/vite-plugin-svelte

# pnpm
pnpm add --save-dev vite@^6 tsx @sveltejs/vite-plugin-svelte

Quickstart

Here's the minimal setup to get a working Forge application running:

1. Create your Express server (server.ts)

import express from 'express';
import { createServer } from '@noego/forge/server';

const app = express();

const options = {
  component_dir: 'components',
  renderer: 'index.html',
  open_api_path: 'openapi.yaml',
};

await createServer(app, options);

export default app;

2. Create an entry file (dev.ts)

import app from './server';

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`🚀 Listening at http://localhost:${PORT}`);
});

3. Create your OpenAPI spec (openapi.yaml)

openapi: '3.0.3'
info:
  title: My App
  version: '1.0.0'

x-fallback-view: error/404.svelte

paths:
  /:
    get:
      summary: Home page
      x-view: views/home.svelte
      x-layout:
        - layout/main.svelte

4. Create components

<!-- components/layout/main.svelte -->
<script>
  let { children } = $props();
</script>

<nav>
  <a href="/">Home</a>
</nav>
<main>
  {@render children()}
</main>

<!-- components/views/home.svelte -->
<h1>Welcome to Forge!</h1>

<!-- components/error/404.svelte -->
<h1>Page not found</h1>

5. Create HTML template (index.html)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Forge App</title>
  <script type="module" src="/@vite/client"></script>
  <style>{{{CSS}}}</style>
  {{{HEAD}}}
</head>
<body>
  <div id="app">{{{APP}}}</div>
  <script>window.__INITIAL_DATA__ = {{{DATA}}};</script>
  <script type="module" src="/client.ts"></script>
</body>
</html>

6. Create client entry (client.ts)

import { createApp } from '@noego/forge/client';

const options = {
  component_dir: 'components',
  renderer: 'index.html',
  open_api_path: 'openapi.yaml',
};

document.addEventListener('DOMContentLoaded', () => {
  createApp(document.getElementById('app'), options);
});

7. Add Vite config (vite.config.js)

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte()],
});

8. Add npm scripts to package.json

{
  "scripts": {
    "dev": "tsx dev.ts",
    "watch": "tsx watch --ignore '**/*.svelte' dev.ts",
    "build:client": "vite build",
    "build:ssr": "SSR=true vite build --ssr",
    "build": "npm run build:client && npm run build:ssr",
    "typecheck": "tsc --noEmit && svelte-check"
  }
}

9. Run the development server

npm run dev

Visit http://localhost:3000 in your browser. Vite is running as middleware inside Express, so hot-module reloading (HMR) and SSR both live behind the same port.

Example Project Structure

Here's a recommended full-stack layout:

.
├─ components/           # Svelte components
│  ├─ layout/
│  ├─ views/
│  └─ error/
├─ openapi.yaml         # OpenAPI specification
├─ index.html           # HTML template
├─ client.ts            # Client entry point
├─ server.ts            # Server entry point
├─ dev.ts               # Development entry
├─ vite.config.js       # Vite configuration
├─ package.json
└─ tsconfig.json

Feel free to organize files differently – the important thing is that paths in your options object point to the correct locations.

Usage

Defining Routes with OpenAPI

Forge extends OpenAPI 3 with custom x-* vendor extensions to define your application structure. Every route in your application is declared in openapi.yaml:

Key OpenAPI Extensions

| Extension | Description | |-----------|-------------| | x-view | Path to the Svelte component that renders the page view | | x-layout | Array of layout components (rendered outside-in) that wrap the view | | x-fallback-view | Component to render for 404 errors | | x-fallback-layout | Default layout when none is specified | | x-middleware | Array of middleware functions to run before rendering |

Important Conventions

  1. Landing page must be explicit – Always declare the / path manually. Forge will not create a default route for you.
  2. x-layout is always an array – Even with one layout component. This keeps types consistent and lets you add more wrappers later without breaking deployed code.

Example OpenAPI Schema

openapi: '3.0.3'
info:
  title: My App
  version: '1.0.0'

x-fallback-view: error/404.svelte

paths:
  /:
    get:
      summary: Home page
      x-view: views/home.svelte
      x-layout:
        - layout/main.svelte

  '/user/:id':
    get:
      summary: User profile
      x-view: views/user_page.svelte
      x-layout:
        - layout/main.svelte

  '/products':
    get:
      summary: Product listing
      x-view: views/products.svelte
      x-layout:
        - layout/main.svelte
        - layout/products_layout.svelte

Path Resolution

Forge resolves paths relative to process.cwd() (your project root). Use one of these formats:

Relative paths (recommended):

const options = {
  component_dir: 'components',           // ✅ Correct
  renderer: 'index.html',
  open_api_path: 'openapi.yaml',
};

Root-relative paths (absolute in the project root):

const options = {
  component_dir: '/frontend/components',  // ✅ Also correct
  renderer: '/frontend/index.html',
};

Avoid relative paths with ./ prefix:

const options = {
  component_dir: './components',          // ❌ Breaks when Node is invoked from different directories
};

HTML Template Placeholders

Your renderer (typically index.html) must contain these three placeholders:

| Placeholder | Content | |-------------|---------| | {{{APP}}} | Server-rendered HTML from your Svelte components | | {{{HEAD}}} | Head tags generated by Svelte (meta, stylesheets, etc.) | | {{{DATA}}} | Initial data from load() functions, serialized as JSON |

Configuration Options

Create a central options file (typically forge.config.ts or forge.config.js):

import type { ServerOptions } from '@noego/forge/options';

export const options: ServerOptions = {
  // Required
  component_dir: 'components',
  renderer: 'index.html',
  open_api_path: 'openapi.yaml',

  // Optional
  development: process.env.NODE_ENV !== 'production',
  build_dir: 'dist_ssr',
  manifest_endpoint: '/manifest.json',
  middleware_path: 'middleware',

  // Static assets mapping
  assets: {
    '/assets': ['public'],
    '/images': ['resources/images'],
  }
};

Component Data Loading

Components can export a server-only load() function to fetch and prepare data before rendering:

<!-- components/views/user.svelte -->
<script lang="ts">
  export let data: any;

  export async function load(request) {
    const { params, query } = request;

    // Fetch data from your API or database
    const user = await fetchUser(params.id);

    return {
      user,
      debug: { query }
    };
  }
</script>

<h1>{data.user.name}</h1>

The load() function receives a RequestData object with:

| Property | Description | |----------|-------------| | url | Full request URL | | params | Dynamic route parameters (e.g., { id: '123' }) | | query | Query string parameters | | headers | Request headers | | body | Parsed request body (POST/PUT/etc.) | | context | Per-request context bag (mutated by middleware) |

Forge behavior:

  1. During SSR: load() is called on the server, data is embedded in the HTML
  2. During client-side navigation: Forge automatically fetches the data as JSON from the same URL
  3. The component receives identical data in both cases

Nested Layouts

Layouts are composed in the order specified, wrapping each other:

paths:
  '/admin/users':
    get:
      x-view: views/admin/users_list.svelte
      x-layout:
        - layout/main.svelte           # outer layout
        - layout/admin_layout.svelte   # middle layout
        - layout/sidebar.svelte        # inner layout

This creates the component tree:

main.svelte
  └── admin_layout.svelte
        └── sidebar.svelte
              └── users_list.svelte (view)

Each layout receives a children() snippet to render inner content.

Static Assets

Map URL paths to directories on disk:

const options = {
  assets: {
    '/assets': ['public'],           // GET /assets/style.css → public/style.css
    '/images': ['resources/images'], // GET /images/logo.png → resources/images/logo.png
  }
};

Important: All directories in assets must exist when the server starts, otherwise Express will throw an error. Use .gitkeep files to commit empty directories.

Client-Side Fetch Middleware

Automatically modify all fetch requests (e.g., add authentication headers):

// client.ts
import { createApp, fetch } from '@noego/forge/client';
import { options } from './forge.config';

// Add a global fetch middleware
fetch.configUpdate((url, init) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    const headers = new Headers(init?.headers);
    headers.set('Authorization', `Bearer ${token}`);
    return { ...init, headers };
  }
  return init;
});

document.addEventListener('DOMContentLoaded', () => {
  createApp(document.getElementById('app'), options);
});

Now all fetch calls automatically include the token:

// No manual headers needed
const response = await fetch('/api/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice' })
});

Advanced Topics

Development vs. Production Behavior

Development mode (npm run dev):

  • Vite runs as middleware inside Express (middlewareMode: true)
  • Hot Module Reloading (HMR) is enabled
  • Svelte components are compiled on-the-fly
  • All requests go through a single Express port

Production mode (NODE_ENV=production):

  • Static pre-built client and SSR bundles are served
  • No Vite middleware (faster, smaller footprint)
  • Use npm run build to generate dist/ and dist_ssr/ directories

Building for Production

  1. Generate production bundles:
npm run build

This runs two Vite builds:

  • Client build: Creates dist/ for browser assets
  • SSR build: Creates dist_ssr/ for server bundle
  1. Serve in production:
NODE_ENV=production node server.js

Your Forge app will serve:

  • Pre-compiled client code from dist/
  • Server-side rendered HTML using dist_ssr/
  • No Vite runtime overhead

Testing with Vitest / Jest

Use supertest to drive your Express server in tests:

import request from 'supertest';
import { buildServer } from '../server';

const app = await buildServer();

it('renders the landing page', async () => {
  const res = await request(app).get('/');
  expect(res.status).toBe(200);
  expect(res.text).toContain('<h1>Welcome');
});

afterAll(() => {
  if (app.close) app.close();
});

Key points:

  • HTML is streamed, so use res.text not res.body
  • Always close the server after tests to release handles
  • buildServer() doesn't call listen(), so you control the lifecycle

Screenshot Testing with ImageRenderer

Forge includes an ImageRenderer for capturing screenshots of your Svelte components. This is useful for visual regression testing.

Prerequisites

Install Playwright as a dev dependency:

npm install --save-dev playwright
npx playwright install chromium

Basic Usage

import { createImageRenderer } from '@noego/forge/test';

const renderer = await createImageRenderer({
  outputDir: './screenshots',
  stitchConfig: './stitch.yaml',
  componentDir: './components',
});

// Capture a route
await renderer.capture('homepage', '/');

// Capture with custom viewport
await renderer.capture('mobile-home', '/', {
  width: 375,
  height: 667,
});

// Capture viewport only (not full page)
await renderer.capture('above-fold', '/', { fullPage: false });

// Don't forget to close when done
await renderer.close();

Loading Static Assets

By default, ImageRenderer cannot load static assets (images, fonts, etc.) because it renders HTML without a server. To enable asset loading, provide an assets mapping:

const renderer = await createImageRenderer({
  outputDir: './screenshots',
  stitchConfig: './stitch.yaml',
  componentDir: './components',
  // Map URL paths to filesystem directories
  assets: {
    '/images': ['ui/resources/images'],
    '/assets': ['public'],
  }
});

This intercepts browser requests and serves files from your local filesystem. The format matches the server's asset configuration.

Configuration Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | outputDir | string | required | Directory to save screenshots | | stitchConfig | string | - | Path to stitch.yaml | | componentDir | string | - | Path to Svelte components | | staticRenderer | StaticRenderer | - | Pre-created StaticRenderer instance | | assets | Record<string, string[]> | - | Asset path mappings | | width | number | 1920 | Default viewport width | | height | number | 1080 | Default viewport height | | deviceScaleFactor | number | 1 | Device pixel ratio (2 for retina) | | format | 'png' \| 'jpeg' | 'png' | Image format | | quality | number | 80 | JPEG quality (0-100) | | waitUntil | string | 'networkidle' | When to consider page loaded | | timeoutMs | number | 10000 | Render timeout |

Capture Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | view | any | - | Data for view component | | layout | any[] | - | Data for layout components | | width | number | config.width | Override viewport width | | height | number | config.height | Override viewport height | | fullPage | boolean | true | Capture full page or viewport only |

Server-Side Middleware

Define custom middleware in x-middleware arrays to run before rendering:

paths:
  '/admin':
    get:
      x-middleware:
        - requireAuth
        - loadUserData
      x-view: views/admin.svelte

Middleware files are loaded from your middleware_path and can modify the request context:

// middleware/requireAuth.ts
export default (request) => {
  if (!request.headers.authorization) {
    throw new Error('Unauthorized');
  }
  request.context.user = parseToken(request.headers.authorization);
};

See docs/server_middleware.md for detailed examples.

Stitch Integration (Modular OpenAPI)

For large projects, split your OpenAPI spec into multiple files with Stitch:

  1. Create stitch.yaml:
stitch:
  - openapi/base.yaml
  - openapi/routes/*.yaml
  - openapi/components/*.yaml
  1. In your client, load YAML via import.meta.glob():
const yamlFiles = import.meta.glob('./openapi/**/*.yaml', {
  query: '?raw',
  import: 'default',
  eager: true
});

await createApp(document.getElementById('app'), {
  open_api_path: 'openapi/stitch.yaml',
  component_dir: 'components'
});

Forge automatically detects stitch files and merges them into a single spec. No build step required – hot reloading works automatically.

Hydration and SSR Debugging

Common hydration mismatch issues:

  • Component uses browser-only APIs (e.g., window, document) without checking typeof window
  • Different data is returned by load() on server vs. client
  • Non-deterministic data (dates, random numbers, etc.)

Debug tips:

// ✅ Safe: check for browser environment
export async function load(request) {
  if (typeof window !== 'undefined') {
    // Client-only code
  }
  return data;
}

// ✅ Deterministic: same data every time
const data = {
  timestamp: '2025-10-22T00:00:00Z',  // Fixed, not Date.now()
  userId: params.id,
};

// ❌ Unsafe: different every render
const data = { id: Math.random() };

Additional Documentation

For deeper topics, check the documentation in /docs:

| Document | Coverage | |----------|----------| | layout_system.md | Composing nested layouts, best practices, common pitfalls | | load_functions.md | Data fetching with load(), SSR and client-side behavior | | state_sharing.md | Reactive global state with Svelte 5 context and $state | | tailwind-layouts.md | Design layouts with Tailwind CSS | | FETCH_MIDDLEWARE.md | Client-side fetch middleware, authentication, security | | server_middleware.md | Per-route server middleware and request context |

Troubleshooting

Common Issues

Components Not Loading

Ensure your component_dir matches the directory structure and is accessible relative to your project root. Verify paths are resolved correctly against process.cwd().

SSR Hydration Errors

Common causes:

  • Components use browser-only APIs (window, document) without checking typeof window !== 'undefined'
  • Component returns different data during SSR vs. client-side navigation
  • Non-deterministic data (random values, current timestamps)

HMR not triggering inside WSL / Docker

File system events behave differently in virtualized environments. Add this to vite.config.js:

export default defineConfig({
  plugins: [svelte()],
  watch: { usePolling: true }
});

"Unexpected token <" / Hydration mismatch warnings

The HTML from the server doesn't match the DOM the client tries to hydrate. Ensure:

  • Component load() functions return identical data on server and client
  • No random values or time-dependent logic in initial render

Route Not Found

Check:

  1. OpenAPI paths are correctly defined in openapi.yaml
  2. Component file paths in x-view and x-layout exist and are relative to component_dir
  3. All referenced component files exist on disk

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

ISC