Getting started
Your first project
From an empty directory to a built site, with every file explained. There are nine of them, and three are a demonstration you delete.
Scaffold
uf new my-sitemy-site is the directory it creates. uf init is the other half — it
scaffolds into the directory you are already in, which is what you want inside
a repository that exists.
Both take an optional template after that, and react is the only one today,
so it is left out here. --lib on either scaffolds a library instead.
They used to be one command, uf create app, whose single optional argument
was a template when it named one and a directory when it did not — so
uf create app react and uf create app my-site did two different things and
neither spelling said which. uf create still works and is no longer listed.
uf new writes a project and nothing else — no node_modules, no lockfile,
no git history. What it writes is:
my-site/
uf.config.js the only config file
app.js the application entry
app/
_uf.layout.js the document: <html>, <head>, <body>
_uf.page.js the page at "/"
Counter.js a client component, as a demonstration
useCounter.js a hook declaration, as a demonstration
_uf.page.test.js a test, as a demonstration
package.json dependencies, and nothing else
.gitignore the generated files, which are not yours to commit
The three marked as a demonstration are there to be read once and deleted. What is left after that is the five files this page explains.
There is no vite.config.ts, no babel.config.js, no .eslintrc, no
tsconfig.json and no vitest.config.ts. That is the point of the exercise.
Run it
uf devThis starts Vite through the host, with uf's plugin installed and your config
translated into Vite's shape. Hot reload works, including React Fast Refresh
through component declarations.
The config
// @flow
import { defineConfig } from "@uniflowed/config";
export default defineConfig({
tasks: {
dev: { command: "uf dev" },
build: { command: "uf build" },
check: { command: "uf check" },
lint: { command: "uf lint" },
fmt: { command: "uf fmt" },
test: { command: "uf test" },
},
});
Nothing in it describes the application: app/ is the route root and app.js
is the entry because those are the defaults. The tasks block is there so
uf run build and your pipeline say the same thing, and
app: { router: { entry, root } } is where you would say otherwise.
defineConfig is a typed identity function: it exists so Flow checks the object
against the config schema at the place you write it, rather than at the place uf
reads it. Every option and its default is in the reference.
The layout and the page
A layout renders the document. It is a component, and it receives the page as
children:
// @flow
export component Layout(children: mixed) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
A page is _uf.page.js, exported as Page:
// @flow
export component Page() {
return <h1>Hello</h1>;
}
The router takes module.default ?? module.Page, so a default export works too
— but uf check warns on a default-exported component, on the grounds that a
route wired by name is easier to find than one wired by position.
Once your dependencies are installed, write children: React.Node instead —
import it with import * as React from "@uniflowed/react". That is the type a
layout's children actually have, and uf check reads it: it finds
@uniflowed/react's manifest in node_modules, follows its exports map, and
types children against what the package declares. It did not always;
#248 and
#403 are that history.
The scaffold writes mixed because it runs before uf install, and
until then @uniflowed/react resolves to nothing — an annotation written
against it would be an error in the first file uf ever wrote for you. mixed
is what a document shell can say with nothing installed, and an intrinsic
element takes it.
The scaffold also wraps the page in no <Suspense>. One with fallback={null}
around every page is worse than none: a page that suspends under it renders as
an empty document instead of failing the way React says it should, and nothing
says so. Routing covers declaring a boundary where a route
wants one.
Both files are Flow, both use component syntax, and neither needs a plugin
declared anywhere. Routing covers nesting, parameters and
loaders, and the tutorial builds all of it into one
application.
Build it
uf buildproduces static HTML for every route it can prerender, plus the client bundle,
under dist/. Add --size-report to see what each chunk cost.
Everything else
uf checkruns Flow over the project. uf fmt formats it, uf lint lints it, and
uf test runs the tests. None of those need to be configured first.