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

create-assemora

v0.2.6

Published

Scaffold an Assemora project: pnpm create assemora my-project

Readme

create-assemora

How a project starts (SPEC.md §78, and the first line of §124).

pnpm create assemora my-project
Project name          my-project
Database URL          postgres://localhost:5432/my_project
Include Studio?       (Y/n)
Include Pages?        (Y/n)
Include MCP?          (Y/n)

Every question has a flag that answers it — --database, --no-studio, --no-pages, --no-mcp — and --yes answers all of them. When stdin is not a terminal nothing is asked at all: it takes the defaults and says on stdout which ones it took, because a scaffolder that blocks for ever waiting for an answer nobody can type is a scaffolder that hangs a build.

It finishes by printing the three commands to run next, in order.

It has no dependencies

Not few — none, and pnpm boundaries fails the build if one appears, in devDependencies too. pnpm create runs this package before anything is installed, so a dependency of its own would have to be fetched first. Node 24 builtins are the whole toolkit: node:fs/promises, node:path, node:readline/promises, node:url.

The public API

import { scaffold } from 'create-assemora'

const { directory, files } = await scaffold({
  name: 'my-project',
  directory: '/Users/ada/my-project',
  database: 'postgres://localhost:5432/my_project',
  studio: true,
  pages: true,
  mcp: false,
})

assemora new in @assemora/cli calls exactly this. It is the convenience, not a second implementation: two scaffolders would disagree about what a project is the first time either one changed.

template takes a starter's name or an absolute path to one, and force writes into a directory that is not empty.

Where the template comes from

starters/<name> in the framework repository, which is a real workspace package so that CI proves it still compiles (ADR-0021). A published tarball cannot reach outside itself, so prepack copies starters/ into templates/ and the resolver looks there first — the packed copy is what a published install has, the workspace copy is what a checkout has. templates/ is machine-made and gitignored; every edit belongs in the starter.

Writing a starter

A starter is an ordinary workspace package. Five conventions turn it into a template.

A dotfile is carried under a leading _, at the root. npm strips a real .gitignore out of a published tarball, so a template holding one would scaffold a project without it, and the same is true of .npmrc. A leading _ on the first path segment becomes .:

_gitignore               →  .gitignore
_npmrc                   →  .npmrc
_github/workflows/ci.yml →  .github/workflows/ci.yml
src/_internal.ts         →  src/_internal.ts     (unchanged)
pages/_app.tsx           →  pages/_app.tsx       (unchanged)

Only the root, because every dotfile npm strips is a root-level one and _ means something else everywhere below it: pages/_app.tsx is Next.js's own spelling and app/_components/ is an ordinary private-folder convention. Rewriting those produced dotfiles that nothing could import.

.env.example needs no such treatment and is copied as it stands. A .env is written only when a database URL was actually given, and it holds that one variable — an example's values are placeholders, and a .env full of placeholder credentials is a file somebody eventually commits believing it to be real.

package.json is rewritten, not copied. The project takes its own name, every workspace:* becomes a range a package manager outside this repository can resolve, and private is left exactly as the starter declared it. A package.json below the root keeps its own name and still loses its workspace ranges.

A file or a dependency that only exists for one answer is named in template.json, and so is the line the template says about itself.

{
  "description": "an empty project: authentication, Studio and nothing declared yet",
  "features": {
    "studio": { "files": ["src/studio.ts"], "dependencies": ["@assemora/studio"] },
    "pages": { "files": ["src/blocks"], "dependencies": ["@assemora/pages"] },
    "mcp": { "files": ["src/mcp-routes.ts"], "dependencies": ["@assemora/mcp"] }
  }
}

Paths are template-relative and a directory counts. template.json is never copied into a project. A feature name it does not recognise is a failed scaffold, not a silently ignored line.

description is one line, lower case, no full stop — it is read as the tail of a line rather than as a sentence. --help prints it beside the name, and so does the block after a scaffold that took the default template, which is how somebody who wanted the worked example rather than the empty default finds out it exists. Which starter to copy is deliberately not a sixth question: SPEC.md §78 fixes the five, and asking everybody about something most people want the default of buys nothing that a printed line does not. listTemplates() reads the list off disk, so a starter added to starters/ is listed the day it lands.

The template's own .gitignore says what its tooling writes. A starter carries it as _gitignore, and everything it names is excluded from the project — .next/, out/, .svelte-kit/, whatever the starter's build tool happens to produce. That is the list the starter's author already maintains, and reading it is what makes a new starter correct on the day it lands rather than the day somebody remembers to teach this package another name.

The other half of the rule is NEVER_COPIED in src/exclusions.ts, which is one list, exported, and read by scripts/copy-templates.mjs too. It names only what no .gitignore can be asked to name: what a checkout of a workspace package accumulates (node_modules/, dist/, .turbo/, coverage/, *.tsbuildinfo, .git/), and what running an Assemora project writes (.assemora/, database/migrations/*.sql, openapi.json, src/generated/). A real project commits those four, so no .gitignore can exclude them — and a project that inherited them would begin life with a migration it did not generate, whose first db:generate writes every table a second time.

It also names the third kind, which points the other way: *.test.ts, *.test.tsx and their *.test-d.* siblings. A starter's tests belong to this repository rather than to the project made from it — they are how CI proves the template still works, and they import a test runner a scaffolded project has no dependency on, so a project that inherited one would not typecheck. A .gitignore cannot say this: a real project commits its tests.

Anything smaller than a file is fenced with a marker comment. A marker is recognised by its text rather than by its comment syntax, and the whole line goes — so the same two words work in TypeScript, in Markdown, in an .env.example and in YAML:

// assemora:if studio
import { studio } from './studio.js'
// assemora:end
<!-- assemora:if !pages -->
This project has no page builder.
<!-- assemora:end -->

! inverts. Regions nest. A misspelt feature name, an unclosed region and an assemora:end that closes nothing are all refused with the file and the line, because each of them would otherwise delete a region for ever — or keep one for ever — and nothing downstream would notice.

A marker owns its line: with anything but comment punctuation beside it, the line is content. A bundle or a source map quoting a fenced file is copied rather than edited, which is what a scaffolder's user needs — they cannot fix a template they have never seen. And assemora:\if is the escape, written into the project without its backslash, so a document explaining this mechanism can print the syntax it explains:

    // assemora:\if studio      in the template
    // assemora:if studio       in the project

package.json is the one file markers stay out of, since JSON cannot carry a comment. That is what the manifest is for.

Three yes/no questions make one starter eight projects, and every one of them has to compile: no import of a file that was left out, no dependency on a package that was left out. src/scaffold.test.ts asserts all eight against a synthetic template, and src/starters.test.ts asserts all eight against every real starter in starters/, in whatever state this checkout has left it — built, installed, or run. --template nextjs shipped unable to scaffold at all because nothing did the second one. That second list is read off disk rather than written out, so a starter added to starters/ is covered by all eight the day it lands.

What a generated project can install today

workspace:* becomes ^<this package's own version>: the scaffolder and the framework ship from the same repository at the same version, so [email protected] writes a project against ^0.4.2. dependencyRange() in src/package-json.ts is the one line to change if that is ever wrong.

Today that version is 0.0.0 and none of the @assemora packages are on npm, so a generated project cannot be installed yet at all. The command says so in as many words rather than leaving somebody to read it out of a resolver error, and that line goes the day there is a release.