Scaffold a small local todo app with TinyJoin and Vite:
npm create tinyjoin@latestThe generated project is ready to run at its root and needs no database server, account, or credentials. Choose TypeScript or JavaScript; SQL, the Drizzle ORM, or the Kysely query builder for its queries; and whether its todos should remain after reloads or start fresh each time.
Your app should look something like this:
- Task list with add/complete/delete
- Single TinyJoin database with one
todostable - Demonstrates basic CRUD operations in SQL, or with Drizzle or Kysely
- Automatic multi-tab access for saved data
- Offline production app loading after the first online visit
- Perfect starter example
It shares its markup and styling with the equivalent create-tinybase vanilla TypeScript starter, with the TinyJoin accent color and system fonts.
For agents and CI, every option can be supplied non-interactively:
npm create tinyjoin@latest -- --non-interactive \
--projectName my-tinyjoin-app \
--language typescript \
--queries sql \
--storage opfs \
--installAndRun falseUse --list-options for the machine-readable option catalog or --help for
usage.
npm run build assembles the publishable create-tinyjoin package under
dist/.
The generated project is a single Vite application at its root (.ts below
becomes .js when JavaScript is chosen):
index.htmlloads the app and defines its theme variables.vite.config.tsenables production offline loading throughtinyjoinOffline().src/index.tsbootstraps the app on load.src/app.tsbuilds the app shell, showing a spinner until the database opens.src/database.tsopens the database once, and closes it with the page.src/todos.tssets up thetodostable, adds the starter todos on the first visit, and runs every query the interface needs, behind one small API.src/schema.ts, with Drizzle, is the Drizzle schema of thetodostable.src/todoInput.ts,src/todoList.ts, andsrc/todoItem.tsare the todo interface, each with a colocated stylesheet.src/topBar.ts,src/title.ts,src/info.ts,src/loading.ts,src/button.ts, andsrc/input.tsare the shared interface pieces.
The three choices of queries generate the same app, whose interface calls the
API that createTodos() in src/todos.ts returns. Only that module, the
dependencies, and the docs differ:
- SQL runs parameterized statements on the TinyJoin client. One atomic script creates the table and adds the starter todos.
- Drizzle declares the table in
src/schema.tsand queries it with the Drizzle ORM throughtinyjoin/drizzle.push()makes the database hold the schema as each tab starts, and the starter todos are added only by the tab whosepush()creates the table. - Kysely queries the table with the Kysely query builder through
tinyjoin/kysely. Kysely'sMigratorruns the app's migrations as each tab starts, one tab at a time; the first creates the table and adds the starter todos.
So in each, tabs opened together add the starter todos once, and a list emptied by the user stays empty. Drizzle adds about 21 KB of compressed script to the page, and Kysely about 31 KB; the Worker and the database engine are the same.
The templates are written once, in TypeScript. Choosing JavaScript strips their
types with ts-blank-space and renames the outputs to .js, exactly as
create-tinybase does: getFiles, and processIncludedFile for the files
that templates include, mark each .ts template transpile when the
JavaScript language is chosen, and tinycreate blanks the types during
post-processing. A JavaScript project also drops tsconfig.json, the
typescript devDependency, and the tsc --noEmit step from its build script.
As in create-tinybase, every file that depends on an option is included by a
template, with includeFile, inside the condition that needs it, rather than
chosen in src/cli.ts: the todos module and Drizzle schema by
app.ts.hbs and todos.drizzle.ts.hbs, the language icon by info.ts.hbs,
and tsconfig.json by package.json.hbs.
Generated applications use tinyjoinOffline() from tinyjoin/vite. The
production build includes the application and complete database runtime in
its offline copy, including files loaded only when persistent storage opens.
There are no remote font requests. Development remains a normal Vite session;
use npm run build and npm run preview to test offline behavior.
New application versions wait for open tabs to close before activating. This keeps existing pages on their matching build; it does not migrate database schemas. Applications with their own service worker can use the plugin's manifest mode. Offline loading and saved local data do not synchronize with a server or another device.
npm run spell # cspell over the sources, templates, and docs
npm run typecheck # generator and test TypeScript
npm test # CLI, template, snapshot, and package-boundary tests
npm run test:generated # generated apps against published TinyJoin
npm run test:e2e # real generated-app behavior in Chromiumnpm run publishPackage runs all of the above before publishing. Add project
vocabulary to cspell.json rather than disabling the check.
npm test compares every generated file, for both languages with both storage
modes, and with each query builder, against the snapshots in
test/__snapshots__. Run npx vitest run test/cli.test.ts -u to accept
intentional template changes. npm run test:generated installs and builds
eight apps: every language and storage combination with SQL, and each language
with each query builder and saved data. npm run test:e2e then drives the six
saved apps in Chromium and tests offline production loading for all eight.
Run the local generator from this repository's parent directory with:
npm --prefix create-tinyjoin run localThis builds only the generator. The generated app installs the published TinyJoin package, so its native build toolchain is not required.
To test unpublished changes from a neighboring TinyJoin source repository, use:
npm --prefix create-tinyjoin run local:sourceThat command builds and packs the sibling TinyJoin repository before starting the generator.
