@lovrozagar/flare 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1200 -2
- package/package.json +27 -2
package/README.md
CHANGED
|
@@ -1,11 +1,1209 @@
|
|
|
1
1
|
# Flare
|
|
2
2
|
|
|
3
|
-
Solid meta-framework.
|
|
3
|
+
Solid + Vite meta-framework. You declare pages and layouts with a typed builder. The server streams HTML (`renderToStream`). Later navigations speak NDJSON (`x-d: 1`). Prefetch, ISR, deferred loaders, and forms reuse that protocol.
|
|
4
|
+
|
|
5
|
+
This repo is the source of [`@lovrozagar/flare`](https://www.npmjs.com/package/@lovrozagar/flare) `0.1.1`. The CLI binary is `flare`.
|
|
6
|
+
|
|
7
|
+
If you are an agent: read this file end to end. It is the usage contract. Import only from the paths in [Package exports](#package-exports). Do not invent a kitchen-sink `flare` barrel for UI.
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Start](#start)
|
|
12
|
+
- [What Flare is](#what-flare-is)
|
|
13
|
+
- [Install](#install)
|
|
14
|
+
- [First app](#first-app)
|
|
15
|
+
- [App anatomy](#app-anatomy)
|
|
16
|
+
- [CLI](#cli)
|
|
17
|
+
- [Routes](#routes)
|
|
18
|
+
- [Route builder](#route-builder)
|
|
19
|
+
- [Params, search, and input](#params-search-and-input)
|
|
20
|
+
- [Layouts, outlet, path segments](#layouts-outlet-path-segments)
|
|
21
|
+
- [Loaders and streaming](#loaders-and-streaming)
|
|
22
|
+
- [Hooks](#hooks)
|
|
23
|
+
- [Auth](#auth)
|
|
24
|
+
- [Errors and redirects](#errors-and-redirects)
|
|
25
|
+
- [Cache](#cache)
|
|
26
|
+
- [Head](#head)
|
|
27
|
+
- [Headers and response routes](#headers-and-response-routes)
|
|
28
|
+
- [Navigation](#navigation)
|
|
29
|
+
- [Rewrite](#rewrite)
|
|
30
|
+
- [Intercept](#intercept)
|
|
31
|
+
- [Forms and server functions](#forms-and-server-functions)
|
|
32
|
+
- [Env split functions](#env-split-functions)
|
|
33
|
+
- [Styles](#styles)
|
|
34
|
+
- [Fonts and images](#fonts-and-images)
|
|
35
|
+
- [i18n, theme, direction](#i18n-theme-direction)
|
|
36
|
+
- [Middleware](#middleware)
|
|
37
|
+
- [Mount](#mount)
|
|
38
|
+
- [Security](#security)
|
|
39
|
+
- [Store and revalidation](#store-and-revalidation)
|
|
40
|
+
- [Query](#query)
|
|
41
|
+
- [Lazy](#lazy)
|
|
42
|
+
- [Service worker](#service-worker)
|
|
43
|
+
- [Sitemap and search engines](#sitemap-and-search-engines)
|
|
44
|
+
- [Tracing](#tracing)
|
|
45
|
+
- [Testing](#testing)
|
|
46
|
+
- [NDJSON protocol](#ndjson-protocol)
|
|
47
|
+
- [Duration strings](#duration-strings)
|
|
48
|
+
- [Plugin](#plugin)
|
|
49
|
+
- [Package exports](#package-exports)
|
|
50
|
+
- [Repository layout](#repository-layout)
|
|
51
|
+
- [Develop](#develop)
|
|
52
|
+
- [License](#license)
|
|
53
|
+
|
|
54
|
+
## Start
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bun add @lovrozagar/flare
|
|
58
|
+
flare init
|
|
59
|
+
bun run dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
/* src/routes/_root_.tsx */
|
|
64
|
+
import { createRootLayout } from "@lovrozagar/flare/root-layout";
|
|
65
|
+
import { ResetCSS } from "@lovrozagar/flare/reset-css";
|
|
66
|
+
|
|
67
|
+
export const route = createRootLayout("_root_").render((props) => (
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8" />
|
|
71
|
+
<ResetCSS />
|
|
72
|
+
</head>
|
|
73
|
+
<body>{props.children}</body>
|
|
74
|
+
</html>
|
|
75
|
+
));
|
|
76
|
+
```
|
|
4
77
|
|
|
5
78
|
```ts
|
|
79
|
+
/* src/routes/index.tsx */
|
|
6
80
|
import { createPage } from "@lovrozagar/flare/page";
|
|
81
|
+
import { Link } from "@lovrozagar/flare/link";
|
|
82
|
+
|
|
83
|
+
export const route = createPage("_root_/")
|
|
84
|
+
.loader(() => ({ message: "Hello from Flare" }))
|
|
85
|
+
.head(() => ({ title: "Home" }))
|
|
86
|
+
.render((props) => (
|
|
87
|
+
<main>
|
|
88
|
+
<h1>{props.loaderData.message}</h1>
|
|
89
|
+
<Link to="/about">About</Link>
|
|
90
|
+
</main>
|
|
91
|
+
));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl http://127.0.0.1:5173/
|
|
96
|
+
# streamed HTML document + FlareState for hydration
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`flare init` writes `src/client.tsx`, `src/server.ts`, `src/router.ts`, a root layout, and `vite.config.ts` with `flare()`. `flare generate` (or the Vite plugin on boot/watch) writes `src/_gen/routes.gen.ts` and `src/_gen/types.gen.d.ts`.
|
|
100
|
+
|
|
101
|
+
The same app runs on Node, Bun, Deno, and Cloudflare Workers. Vite is the bundler. Solid is the renderer.
|
|
102
|
+
|
|
103
|
+
## What Flare is
|
|
104
|
+
|
|
105
|
+
- **Builder DX.** `createPage("_root_/about").loader(...).head(...).render(...)`. Types flow from the generated route tree into `Link`, `navigate`, and loader `ctx`.
|
|
106
|
+
- **Server-driven.** Loaders, auth, cache, and head run on the server. The client hydrates, then fetches NDJSON for SPA navigations.
|
|
107
|
+
- **NDJSON only.** Data requests are line-delimited JSON (`t:"l"` loader, `t:"c"` deferred chunk, `t:"h"` head, `t:"r"` redirect). There is no parallel JSON-RPC surface.
|
|
108
|
+
- **Streaming first.** `ctx.defer()` + `<Await>` stream after the shell. HTML never waits for every promise.
|
|
109
|
+
- **One plugin.** `flare()` from `@lovrozagar/flare/plugins` is Vite: codegen, `sx` / `class=` compile, images, server functions, prerender, service worker, dev dashboard.
|
|
110
|
+
- **Web Standards.** Handlers see `Request`. Responses are `Response`. Workers get `waitUntil` via `serverContext` / `background()`.
|
|
111
|
+
|
|
112
|
+
Flare is not Next with Solid bolted on. There is no `getServerSideProps` object. A route is one file and one chain.
|
|
113
|
+
|
|
114
|
+
## Install
|
|
115
|
+
|
|
116
|
+
Consumers need Vite 8 and Solid 1.9. This repo develops on [Bun](https://bun.sh) 1.3+.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bun add @lovrozagar/flare
|
|
120
|
+
# or
|
|
121
|
+
npm add @lovrozagar/flare
|
|
122
|
+
# or
|
|
123
|
+
pnpm add @lovrozagar/flare
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Peers: `solid-js`, `vite`, `vite-plugin-solid`. Optional: `sharp` (images), `isbot` (skip locale cookies for bots), `@tanstack/solid-query` (query), `@tanstack/query-broadcast-client-experimental` (query broadcast), `oxc-parser`, `oxc-resolver`.
|
|
127
|
+
|
|
128
|
+
The CLI is a workspace package (`@flare/cli`, binary `flare`). In this repo it is on the path via workspace linking. A published consumer uses `flare init` after adding the package.
|
|
129
|
+
|
|
130
|
+
## First app
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
mkdir my-app && cd my-app
|
|
134
|
+
bun init -y
|
|
135
|
+
bun add @lovrozagar/flare solid-js vite vite-plugin-solid
|
|
136
|
+
flare init
|
|
137
|
+
bun run dev
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
| File | Role |
|
|
141
|
+
| ------------------------- | ----------------------------------------------------------- |
|
|
142
|
+
| `src/client.tsx` | `createClient(() => router)` |
|
|
143
|
+
| `src/server.ts` | `createServer(router)` — Vite SSR / Workers `fetch` |
|
|
144
|
+
| `src/router.ts` | `createRouter({ layouts, routeTree })` |
|
|
145
|
+
| `src/routes/_root_.tsx` | Root HTML document |
|
|
146
|
+
| `src/routes/index.tsx` | Home page |
|
|
147
|
+
| `vite.config.ts` | `flare()` plugin |
|
|
148
|
+
| `src/_gen/routes.gen.ts` | Generated tree + lazy imports (do not hand-edit) |
|
|
149
|
+
| `src/_gen/types.gen.d.ts` | `/// <reference types="@lovrozagar/flare/virtual-types" />` |
|
|
150
|
+
|
|
151
|
+
Scripts: `dev` (`vite dev`), `build`, `preview`, `generate` (`flare generate`).
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
flare init --template saas
|
|
155
|
+
flare init --template blog
|
|
156
|
+
flare init --template marketing
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Flags: `--auth cookie|jwt|none`, `--cache isr|ssg|ssr|mixed`, `--style tailwind|css-modules|none`, `--locale en,hr`. Existing files refuse overwrite unless you pass `--force`.
|
|
160
|
+
|
|
161
|
+
## App anatomy
|
|
162
|
+
|
|
163
|
+
### Client
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { createClient } from "@lovrozagar/flare/client";
|
|
167
|
+
import { router } from "./router";
|
|
168
|
+
|
|
169
|
+
createClient(() => router)
|
|
170
|
+
.onReady((ctx) => {
|
|
171
|
+
/* ctx.navigate, ctx.invalidate, ctx.navigationPhase */
|
|
172
|
+
})
|
|
173
|
+
.onHydrated(() => {})
|
|
174
|
+
.onIdle(() => {})
|
|
175
|
+
.onInteraction(() => {});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`createClient` hydrates on the next microtask. Or call `hydrate(router)` from `@lovrozagar/flare/hydrate` yourself.
|
|
179
|
+
|
|
180
|
+
### Router
|
|
181
|
+
|
|
182
|
+
```ts
|
|
7
183
|
import { createRouter } from "@lovrozagar/flare/router";
|
|
184
|
+
import { layouts, routeTree } from "./_gen/routes.gen";
|
|
185
|
+
|
|
186
|
+
export const router = createRouter({
|
|
187
|
+
layouts,
|
|
188
|
+
routeTree,
|
|
189
|
+
basePath: undefined,
|
|
190
|
+
caseSensitive: false,
|
|
191
|
+
notFoundMode: "fuzzy",
|
|
192
|
+
trailingSlash: "preserve",
|
|
193
|
+
scrollRestoration: true,
|
|
194
|
+
viewTransitions: true,
|
|
195
|
+
cache: { client: { prefetch: "intent", staleTime: 30_000 } },
|
|
196
|
+
locale: { defaultLocale: "en", locales: ["en", "hr"], paramName: "locale" },
|
|
197
|
+
theme: { defaultTheme: "system" },
|
|
198
|
+
direction: { defaultDir: "ltr" },
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| Option | Meaning |
|
|
203
|
+
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
204
|
+
| `layouts` / `routeTree` | From `src/_gen/routes.gen.ts` |
|
|
205
|
+
| `basePath` | App mounted under `/app` |
|
|
206
|
+
| `caseSensitive` | URL matching |
|
|
207
|
+
| `notFoundMode` | `"fuzzy"` hydrate nearest match; `"root"` use `_root_/` |
|
|
208
|
+
| `trailingSlash` | `"always"` \| `"never"` \| `"preserve"` |
|
|
209
|
+
| `scrollRestoration` / `getScrollRestorationKey` / `scrollRestorationBehavior` / `scrollRestorationMaxEntries` | History scroll |
|
|
210
|
+
| `routeCacheMaxEntries` | Client match cache size |
|
|
211
|
+
| `rewrite` | Vanity URLs — see [Rewrite](#rewrite) |
|
|
212
|
+
| `queryClientGetter` | TanStack — see [Query](#query) |
|
|
213
|
+
|
|
214
|
+
### Server
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import { createServer } from "@lovrozagar/flare/server";
|
|
218
|
+
import { router } from "./router";
|
|
219
|
+
|
|
220
|
+
export const server = createServer(router)
|
|
221
|
+
.authenticateFn(async ({ request }) => {
|
|
222
|
+
const session = request.headers.get("cookie");
|
|
223
|
+
return session ? { id: "user-1" } : null;
|
|
224
|
+
})
|
|
225
|
+
.serverContext(({ request }) => ({ requestId: request.headers.get("x-request-id") ?? "" }))
|
|
226
|
+
.security({ "X-Frame-Options": "DENY" })
|
|
227
|
+
.cache({ store })
|
|
228
|
+
.keepalive({ interval: 30_000 })
|
|
229
|
+
.use("/api/*", apiProxy({ target: "https://api.example.com" }))
|
|
230
|
+
.mount("/rpc", (request) => new Response("ok"))
|
|
231
|
+
.sitemap({ origin: "https://example.com" })
|
|
232
|
+
.tracing({ timing: true });
|
|
233
|
+
|
|
234
|
+
export default {
|
|
235
|
+
fetch(request: Request, env?: unknown, ctx?: { waitUntil?: (p: Promise<unknown>) => void }) {
|
|
236
|
+
return server.fetch(request, env, ctx);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`server.fetch` is the Vite SSR handler and the Workers `fetch` export. `getStaticParams()` feeds prerender. `background(promise)` from `@lovrozagar/flare/server-context` binds `waitUntil` so ISR work cannot 500 a finished render.
|
|
242
|
+
|
|
243
|
+
## CLI
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
flare init [--template saas|blog|marketing] [--auth] [--cache] [--style] [--force]
|
|
247
|
+
flare generate | flare gen [--watch]
|
|
248
|
+
flare add auth|cache|loader|head|input|error-boundary <paths...>
|
|
249
|
+
flare font add|list|info|remove
|
|
250
|
+
flare routes
|
|
251
|
+
flare status
|
|
252
|
+
flare validate
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`plan`, `remove`, `rename`, `setup-ai` are reserved and not implemented.
|
|
256
|
+
|
|
257
|
+
### `flare generate` / `flare gen`
|
|
258
|
+
|
|
259
|
+
Scans `src/routes/` and writes `src/_gen/`. The Vite plugin also runs this on boot and on watch. `--watch` regenerates when route files change.
|
|
260
|
+
|
|
261
|
+
`codegen.fsVirtualPaths` (plugin default `true`) requires suffix files. Set `fsVirtualPaths: false` to keep handwritten `createPage("_root_/about")` strings (the product e2e app does this).
|
|
262
|
+
|
|
263
|
+
### `flare add`
|
|
264
|
+
|
|
265
|
+
Patches an existing route chain: `auth`, `cache [--isr N] [--ssg]`, `loader`, `head`, `input`, `error-boundary`.
|
|
266
|
+
|
|
267
|
+
### `flare font`
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
flare font add --name Inter
|
|
271
|
+
flare font list
|
|
272
|
+
flare font info Inter
|
|
273
|
+
flare font remove Inter
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Writes `public/fonts/` and prints `import { inter } from "@lovrozagar/flare/fonts/inter"`.
|
|
277
|
+
|
|
278
|
+
### `flare routes` / `status` / `validate`
|
|
279
|
+
|
|
280
|
+
- `routes` — print the generated tree.
|
|
281
|
+
- `status` — project health (flare dep, `_gen`, Vite config).
|
|
282
|
+
- `validate` — chain / file conventions.
|
|
283
|
+
|
|
284
|
+
## Routes
|
|
285
|
+
|
|
286
|
+
### String virtual paths
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { createPage } from "@lovrozagar/flare/page";
|
|
290
|
+
|
|
291
|
+
export const route = createPage("_root_/blog/[slug]")
|
|
292
|
+
.loader((ctx) => ({ slug: ctx.location.params.slug }))
|
|
293
|
+
.render((props) => <h1>{props.loaderData.slug}</h1>);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The string is the virtual path. `_root_` is the root layout scope. Groups like `_root_/(blog)/blog` share a layout without a URL segment.
|
|
297
|
+
|
|
298
|
+
### Filesystem virtual paths
|
|
299
|
+
|
|
300
|
+
With `codegen: { fsVirtualPaths: true }` (plugin default):
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
src/routes/_root_/about/about.page.tsx → createPage("_root_/about")
|
|
304
|
+
src/routes/_root_/(blog)/blog.layout.tsx → createLayout("_root_/(blog)")
|
|
305
|
+
src/routes/_root_/users/[id]/user.page.tsx → createPage("_root_/users/[id]")
|
|
306
|
+
src/routes/_root_/files/[...path]/files.page.tsx
|
|
307
|
+
src/routes/_root_/optional-locale/[[locale]]/opt.page.tsx
|
|
308
|
+
src/routes/_admin_/dashboard/dash.page.tsx → createPage("_admin_/dashboard")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Folders starting with `_` other than `_name_` root scopes are ignored (`_utils`). `[_]internal` is a literal `_internal` URL segment. `ignorePrefix` on the plugin skips extra names.
|
|
312
|
+
|
|
313
|
+
### Multiple roots
|
|
314
|
+
|
|
315
|
+
`_root_` and `_admin_` are separate HTML documents. Crossing from one to the other is a full load, not SPA.
|
|
316
|
+
|
|
317
|
+
## Route builder
|
|
318
|
+
|
|
319
|
+
Chain order (typical): `intercept` → `cache` → `authenticate` → `input` → `effects` → `authorize` → `preloader` → `loader` → `head` / `headers` → `render` or `response` → error slots.
|
|
320
|
+
|
|
321
|
+
| Method | Role |
|
|
322
|
+
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
323
|
+
| `.cache(config)` | Client / SSR / ISR / SSG / CDN |
|
|
324
|
+
| `.authenticate()` / `.authenticate("optional")` / `.authenticate(false)` | Auth gate |
|
|
325
|
+
| `.authorize(fn)` | Role check; inherits parent unless overridden |
|
|
326
|
+
| `.input({ params, searchParams })` | Standard Schema or parse fn |
|
|
327
|
+
| `.effects({ loaderDeps, shouldRefetch })` | When to rerun the loader on search change |
|
|
328
|
+
| `.preloader(fn)` | Runs parent → child before loaders; result is `preloaderContext` |
|
|
329
|
+
| `.loader(fn)` | Server data |
|
|
330
|
+
| `.head(fn)` / `.head(fn, { replace: true })` | Title, meta, JSON-LD |
|
|
331
|
+
| `.headers(fn)` | Extra response headers |
|
|
332
|
+
| `.render(fn)` | Solid UI |
|
|
333
|
+
| `.response(fn)` | Raw `Response` (sitemap.xml, robots) — no HTML |
|
|
334
|
+
| `.intercept({ from, render })` | Modal / drawer over another route |
|
|
335
|
+
| `.redirect({ to, status })` | Static redirect route |
|
|
336
|
+
| `.middleware(...)` | Per-route middleware |
|
|
337
|
+
| `.errorRender()` / `.notFoundRender()` / `.unauthenticatedRender()` / `.unauthorizedRender()` | Boundaries |
|
|
338
|
+
|
|
339
|
+
`createLayout` and `createRootLayout` share the same idea. Root layouts own `<html>`. `createPathSegment` is URL-only (no UI) — used for `[[locale]]`.
|
|
340
|
+
|
|
341
|
+
Loader / preloader / authorize `ctx` always has: `request`, `location`, `auth`, `env`, `locale()`, `serverContext`, `abortController`, plus throw helpers (`notFound`, `redirect`, `unauthenticated`, `unauthorized`) and URL helpers. Loader `ctx` also has `defer`, `cause` (`"enter"` \| `"prefetch"` \| `"stay"`), `prefetch`, `deps`.
|
|
342
|
+
|
|
343
|
+
## Params, search, and input
|
|
344
|
+
|
|
345
|
+
| Pattern | Type |
|
|
346
|
+
| ------------- | ----------------------- |
|
|
347
|
+
| `[id]` | `string` |
|
|
348
|
+
| `[...path]` | `string[]` |
|
|
349
|
+
| `[[locale]]` | `string \| undefined` |
|
|
350
|
+
| `[[...rest]]` | `string[] \| undefined` |
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { z } from "zod";
|
|
354
|
+
|
|
355
|
+
export const route = createPage("_root_/users/[id]")
|
|
356
|
+
.input({
|
|
357
|
+
params: z.object({ id: z.string().min(1) }),
|
|
358
|
+
searchParams: z.object({ tab: z.string().optional() }),
|
|
359
|
+
})
|
|
360
|
+
.loader((ctx) => ({ id: ctx.location.params.id, tab: ctx.location.search.tab }))
|
|
361
|
+
.render((props) => <p>{props.loaderData.id}</p>);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Validators: Zod, Valibot, ArkType, TypeBox, Yup, Effect, Superstruct, or `{ parse }` / a function. Invalid params are 400/500 depending on the schema path. `Link` and `navigate({ to, params, search })` are typed from `src/_gen/types.gen.d.ts`.
|
|
365
|
+
|
|
366
|
+
## Layouts, outlet, path segments
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createLayout } from "@lovrozagar/flare/layout";
|
|
370
|
+
import { Outlet } from "@lovrozagar/flare/outlet";
|
|
371
|
+
|
|
372
|
+
export const route = createLayout("_root_/(blog)")
|
|
373
|
+
.loader(() => ({ section: "Blog" }))
|
|
374
|
+
.render((props) => (
|
|
375
|
+
<div>
|
|
376
|
+
<nav>{props.loaderData?.section}</nav>
|
|
377
|
+
{props.children}
|
|
378
|
+
{/* or <Outlet /> */}
|
|
379
|
+
</div>
|
|
380
|
+
));
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { createPathSegment } from "@lovrozagar/flare/path-segment";
|
|
385
|
+
|
|
386
|
+
export const route = createPathSegment("[[locale]]");
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Child errors bubble to the nearest layout/root that defines `.errorRender()` / `.notFoundRender()` / `.unauthenticatedRender()` / `.unauthorizedRender()`.
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
.errorRender((props) => (
|
|
393
|
+
<main>
|
|
394
|
+
<p>{props.error.message}</p>
|
|
395
|
+
<button type="button" onClick={() => props.retry()}>
|
|
396
|
+
Retry
|
|
397
|
+
</button>
|
|
398
|
+
</main>
|
|
399
|
+
))
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Loaders and streaming
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
import { Await } from "@lovrozagar/flare/await";
|
|
406
|
+
import { createPage } from "@lovrozagar/flare/page";
|
|
407
|
+
|
|
408
|
+
export const route = createPage("_root_/blog/[slug]")
|
|
409
|
+
.preloader((ctx) => ({ locale: ctx.locale() }))
|
|
410
|
+
.loader((ctx) => {
|
|
411
|
+
const slug = ctx.location.params.slug;
|
|
412
|
+
const comments = ctx.defer(async () => db.comments(slug), { key: "comments" });
|
|
413
|
+
return { slug, title: `Post: ${slug}`, comments };
|
|
414
|
+
})
|
|
415
|
+
.render((props) => (
|
|
416
|
+
<main>
|
|
417
|
+
<h1>{props.loaderData.title}</h1>
|
|
418
|
+
<Await pending={<p>Loading…</p>} promise={props.loaderData.comments}>
|
|
419
|
+
{(comments) => <ul>{/* … */}</ul>}
|
|
420
|
+
</Await>
|
|
421
|
+
</main>
|
|
422
|
+
));
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Pipeline: authenticate → authorize → preloaders (parent → child) → loaders. `ctx.defer(fn, { key, prerender: "resolve" | "stream" })` marks a field for NDJSON `t:"c"` chunks. Prefetch skips deferred execution (shell only). History restore of a deferred match refetches so `<Await>` gets a live promise.
|
|
426
|
+
|
|
427
|
+
`<Await>` also accepts `error` and `onError`. Missing deferred data must not throw in `.then` — the component treats a missing promise as pending.
|
|
428
|
+
|
|
429
|
+
## Hooks
|
|
430
|
+
|
|
431
|
+
From `@lovrozagar/flare` (route-builder barrel) / the same names on the provider:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
import {
|
|
435
|
+
useBlocker,
|
|
436
|
+
useLoaderData,
|
|
437
|
+
useLoaderT,
|
|
438
|
+
useLocation,
|
|
439
|
+
useMatch,
|
|
440
|
+
useNavigate,
|
|
441
|
+
useParams,
|
|
442
|
+
usePreloaderContext,
|
|
443
|
+
useSearch,
|
|
444
|
+
} from "@lovrozagar/flare";
|
|
445
|
+
|
|
446
|
+
const data = useLoaderData({ from: "_root_/about" });
|
|
447
|
+
const params = useParams({ from: "_root_/users/[id]" });
|
|
448
|
+
const search = useSearch({ from: "_root_/users/[id]" });
|
|
449
|
+
const navigate = useNavigate();
|
|
450
|
+
const blocker = useBlocker(() => dirty());
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`from` is a virtual path, not a URL. Accessors are Solid signals.
|
|
454
|
+
|
|
455
|
+
## Auth
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
export const route = createPage("_root_/dashboard")
|
|
459
|
+
.authenticate()
|
|
460
|
+
.authorize(({ user }) => user.role === "admin")
|
|
461
|
+
.render(() => <h1>Dashboard</h1>);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- `.authenticate()` — required; null user → `UnauthenticatedError` (401).
|
|
465
|
+
- `.authenticate("optional")` — user may be null.
|
|
466
|
+
- `.authenticate(false)` — skip even if a parent required it.
|
|
467
|
+
- Child inherits parent auth unless it sets its own mode.
|
|
468
|
+
- `createServer(router).authenticateFn(fn)` is the app-wide hook (cookie, header, JWT).
|
|
469
|
+
|
|
470
|
+
Throw helpers: `ctx.notFound()`, `ctx.redirect({ to, params, search, status, replace })`, `ctx.unauthenticated()`, `ctx.unauthorized()`.
|
|
471
|
+
|
|
472
|
+
## Errors and redirects
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import {
|
|
476
|
+
NotFoundError,
|
|
477
|
+
RedirectResponse,
|
|
478
|
+
UnauthenticatedError,
|
|
479
|
+
UnauthorizedError,
|
|
480
|
+
isNotFoundError,
|
|
481
|
+
isRedirectResponse,
|
|
482
|
+
} from "@lovrozagar/flare/errors";
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Default redirect status is **303**. `.redirect({ to, status: 307 })` overrides. External: `{ href: "https://example.com" }`. `javascript:` is rejected.
|
|
486
|
+
|
|
487
|
+
## Cache
|
|
488
|
+
|
|
489
|
+
### Client
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
createRouter({
|
|
493
|
+
cache: { client: { prefetch: "intent", staleTime: 30_000, gcTime: "5m" } },
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
export const route = createPage("_root_/about").cache({
|
|
497
|
+
client: { staleTime: "10s", prefetch: "viewport", cacheDeferred: true },
|
|
498
|
+
});
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`prefetch`: `false` | `"intent"` | `"viewport"` | `"render"`. `staleTime` / `gcTime` / `prefetchStaleTime` accept ms or [duration strings](#duration-strings). `hasDeferred` cache entries are treated stale so popstate does not replay a dead marker. `client: false` turns client cache off for that route.
|
|
502
|
+
|
|
503
|
+
### SSR store / ISR / SSG
|
|
504
|
+
|
|
505
|
+
`ssr`, `isr`, and `ssg` are mutually exclusive.
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
.cache({
|
|
509
|
+
ssr: { staleTime: 5_000, ttl: 60, tags: ["posts"], key: ({ params }) => params.slug },
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
.cache({ ssg: true })
|
|
513
|
+
|
|
514
|
+
.cache({
|
|
515
|
+
ssg: {
|
|
516
|
+
params: () => [{ slug: "hello" }],
|
|
517
|
+
defer: "resolve",
|
|
518
|
+
},
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
.cache({
|
|
522
|
+
isr: {
|
|
523
|
+
revalidate: 60,
|
|
524
|
+
params: () => [{ slug: "hello" }],
|
|
525
|
+
dynamicParams: true,
|
|
526
|
+
},
|
|
527
|
+
})
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
- **SSR cache** — store-backed HTML/data. Needs a `FlareStore` on `createServer(...).cache({ store })`.
|
|
531
|
+
- **SSG** — built at `vite build` when `flare({ prerender: true })`.
|
|
532
|
+
- **ISR** — `{ revalidate }` time-based, or omit `revalidate` for tag-only. `isr: true` is on-demand only. `dynamicParams: false` 404s unlisted params.
|
|
533
|
+
- Tag purge: [Store and revalidation](#store-and-revalidation).
|
|
534
|
+
|
|
535
|
+
HTML that embeds a per-request CSP nonce is never `304`. A 304 would reuse the old body (old nonce) against a new CSP and block inline scripts. ETag still lands on the 200.
|
|
536
|
+
|
|
537
|
+
### CDN
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
.cache({
|
|
541
|
+
cdn: { maxAge: 60, swr: 300, tags: ["posts"], vary: ["Accept-Language"], private: false },
|
|
542
|
+
})
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Sets `Cache-Control` / `CDN-Cache-Control`. Dev can emulate a CDN disk cache (`dev.cdnCache`, default on). Product e2e turns it off so HTML stays fresh.
|
|
546
|
+
|
|
547
|
+
## Head
|
|
548
|
+
|
|
549
|
+
```ts
|
|
550
|
+
.head((ctx) => ({
|
|
551
|
+
title: `About — ${ctx.loaderData.year}`,
|
|
552
|
+
description: "…",
|
|
553
|
+
canonical: "https://example.com/about",
|
|
554
|
+
robots: { index: true, follow: true },
|
|
555
|
+
openGraph: { title: "…", images: [{ url: "…", width: 1200, height: 630 }] },
|
|
556
|
+
twitter: { card: "summary_large_image", title: "…" },
|
|
557
|
+
jsonLd: [{ "@type": "WebPage", name: "About" }],
|
|
558
|
+
icons: { ico: "/favicon.ico", svg: "/icon.svg" },
|
|
559
|
+
css: "/page.css",
|
|
560
|
+
}))
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
`ctx.parentHead` is the merged parent. Child titles win. `.head(fn, { replace: true })` drops inherited description/keywords. SPA navigation applies per-route heads and removes stale meta / JSON-LD.
|
|
564
|
+
|
|
565
|
+
## Headers and response routes
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
.headers((ctx) => ({
|
|
569
|
+
"Cache-Control": "private",
|
|
570
|
+
"X-From": ctx.loaderData.id,
|
|
571
|
+
}))
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
/* src/routes/_root_/sitemap.xml/sitemap.page.tsx */
|
|
576
|
+
export const route = createPage("_root_/sitemap.xml").response(() => {
|
|
577
|
+
return new Response("<urlset>…</urlset>", {
|
|
578
|
+
headers: { "content-type": "application/xml" },
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
`.response()` skips HTML / loaders / head. Use it for `sitemap.xml`, `robots.txt`, feeds.
|
|
584
|
+
|
|
585
|
+
## Navigation
|
|
586
|
+
|
|
587
|
+
```tsx
|
|
588
|
+
import { Link } from "@lovrozagar/flare/link";
|
|
589
|
+
import { navigate } from "@lovrozagar/flare";
|
|
590
|
+
|
|
591
|
+
<Link to="/blog/[slug]" params={{ slug: "hello-world" }} prefetch="intent">
|
|
592
|
+
Post
|
|
593
|
+
</Link>
|
|
594
|
+
|
|
595
|
+
<Link href="https://example.com" target="_blank">
|
|
596
|
+
External
|
|
597
|
+
</Link>
|
|
598
|
+
|
|
599
|
+
<Link to="/about" replace hash="section" disabled />
|
|
600
|
+
|
|
601
|
+
<button
|
|
602
|
+
type="button"
|
|
603
|
+
onClick={() => navigate({ to: "/users/[id]", params: { id: "1" }, search: { tab: "bio" } })}
|
|
604
|
+
>
|
|
605
|
+
Go
|
|
606
|
+
</button>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
- Internal `to` is typed. External `href` is not rewritten.
|
|
610
|
+
- `prefetch={false}` disables. Default comes from router / route cache.
|
|
611
|
+
- `activeClass` / `inactiveClass` / `activeProps` / `inactiveProps` / `aria-current`.
|
|
612
|
+
- `createRouter({ viewTransitions: true })` wraps updates in `document.startViewTransition` (Chromium). Put `<ViewTransitionCSS />` from `@lovrozagar/flare/view-transition-css` in the root head.
|
|
613
|
+
- `useBlocker(() => dirty())` — first-class leave guard.
|
|
614
|
+
- Optional chrome: `<NavigationProgress />` from `@lovrozagar/flare/navigation-progress`.
|
|
615
|
+
|
|
616
|
+
`ctx.invalidate()` / `router.invalidate()` refetches current matches.
|
|
617
|
+
|
|
618
|
+
## Rewrite
|
|
619
|
+
|
|
620
|
+
Vanity URLs without changing the matched virtual path.
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
import type { LocationRewrite } from "@lovrozagar/flare/rewrite";
|
|
624
|
+
|
|
625
|
+
const rewrite: LocationRewrite = {
|
|
626
|
+
input: ({ url }) => {
|
|
627
|
+
if (url.pathname === "/vanity") {
|
|
628
|
+
const next = new URL(url);
|
|
629
|
+
next.pathname = "/about";
|
|
630
|
+
return next;
|
|
631
|
+
}
|
|
632
|
+
return undefined;
|
|
633
|
+
},
|
|
634
|
+
output: ({ url }) => {
|
|
635
|
+
if (url.pathname === "/about") {
|
|
636
|
+
const next = new URL(url);
|
|
637
|
+
next.pathname = "/vanity";
|
|
638
|
+
return next;
|
|
639
|
+
}
|
|
640
|
+
return undefined;
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
createRouter({ rewrite, layouts, routeTree });
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`input` maps the request URL to the real route. `output` maps generated links / redirects back to the vanity URL. Return `undefined` to leave the URL alone.
|
|
648
|
+
|
|
649
|
+
## Intercept
|
|
650
|
+
|
|
651
|
+
Modal / drawer that keeps the background route mounted.
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
createPage("_root_/photo/[id]").intercept({
|
|
655
|
+
from: ["_root_/gallery"],
|
|
656
|
+
render: "_root_/photo/[id]",
|
|
657
|
+
});
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```tsx
|
|
661
|
+
import { InterceptOutlet } from "@lovrozagar/flare/intercept-outlet";
|
|
662
|
+
|
|
663
|
+
<InterceptOutlet />;
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`from` is the background virtual path(s). Closing the intercept returns to that route.
|
|
667
|
+
|
|
668
|
+
## Forms and server functions
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
import { z } from "zod";
|
|
672
|
+
import { createServerFn } from "@lovrozagar/flare/server-fn";
|
|
673
|
+
import { Form, FieldError } from "@lovrozagar/flare/form";
|
|
674
|
+
|
|
675
|
+
const save = createServerFn({ name: "save" })
|
|
676
|
+
.input(z.object({ email: z.string().email() }))
|
|
677
|
+
.handler(async ({ input, auth, revalidate, piggyback }) => {
|
|
678
|
+
await revalidate({ tags: ["contacts"], tiers: ["ssr"] });
|
|
679
|
+
piggyback(["contact", input.email], { email: input.email });
|
|
680
|
+
return { ok: true, email: input.email };
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
<Form
|
|
684
|
+
action={save}
|
|
685
|
+
onSuccess={(data) => console.log(data.email)}
|
|
686
|
+
onError={(err) => console.error(err)}
|
|
687
|
+
>
|
|
688
|
+
{(form) => (
|
|
689
|
+
<>
|
|
690
|
+
<input name="email" value={form.value("email")} />
|
|
691
|
+
<FieldError name="email" />
|
|
692
|
+
<button type="submit" disabled={form.pending()}>
|
|
693
|
+
Save
|
|
694
|
+
</button>
|
|
695
|
+
</>
|
|
696
|
+
)}
|
|
697
|
+
</Form>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
- The Vite plugin strips handler bodies from the client bundle.
|
|
701
|
+
- Progressive enhancement works with JS off (`POST` + redirect).
|
|
702
|
+
- CSRF: mutating methods check `Origin` / `Referer`.
|
|
703
|
+
- `.authenticate()` / `.authorize(fn)` on the server fn (same idea as routes).
|
|
704
|
+
- `.validator()` is an alias of `.input()`.
|
|
705
|
+
- Streaming: `.handler(async function* ({ signal }) { yield chunk })`.
|
|
706
|
+
- `createServerFn({ name, method: "get" })` for idempotent GET RPC.
|
|
707
|
+
- `@lovrozagar/flare/server-fn-query` wires a server fn to TanStack Query.
|
|
708
|
+
|
|
709
|
+
`form.pending()`, `form.error()`, `form.result()`, `form.fieldErrors()`, `form.hasErrors()`, `form.reset()`, `form.value(name)`.
|
|
710
|
+
|
|
711
|
+
## Env split functions
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
import { createServerOnlyFn } from "@lovrozagar/flare/server-only";
|
|
715
|
+
import { createClientOnlyFn } from "@lovrozagar/flare/client-only";
|
|
716
|
+
import { createIsomorphicFn } from "@lovrozagar/flare/isomorphic";
|
|
717
|
+
|
|
718
|
+
const readSecret = createServerOnlyFn(() => process.env.SECRET);
|
|
719
|
+
const measure = createClientOnlyFn(() => performance.now());
|
|
720
|
+
const now = createIsomorphicFn({
|
|
721
|
+
server: () => Date.now(),
|
|
722
|
+
client: () => performance.now(),
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
The plugin drops the unused side from each bundle. Calling a server-only fn on the client throws.
|
|
727
|
+
|
|
728
|
+
## Styles
|
|
729
|
+
|
|
730
|
+
Three supported surfaces:
|
|
731
|
+
|
|
732
|
+
| API | Use |
|
|
733
|
+
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
|
734
|
+
| `class="flex gap-4 p-8"` | Tailwind utilities, compiled when `sx: { tw: true }` |
|
|
735
|
+
| `sx={{ color: "rgb(0,0,255)", padding: "16px", variants: { hover: { opacity: "0.8" } } }}` | Typed style object → atomic classes |
|
|
736
|
+
| `styles("box", { css, state, vars })` | Named scoped CSS, `data-c` attribute |
|
|
737
|
+
|
|
738
|
+
```tsx
|
|
739
|
+
import { styles, cn } from "@lovrozagar/flare/styles";
|
|
740
|
+
|
|
741
|
+
<div class="bg-blue-500 p-4" />
|
|
742
|
+
<div sx={{ color: "rgb(0, 100, 200)", padding: "16px" }} />
|
|
743
|
+
<div {...styles("box", { css: "color: rgb(255, 0, 0)" })} />
|
|
744
|
+
<div class={cn("base", on() && "active")} />
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
`tw=` on `styles()` or as a JSX attribute is **dropped**. Put utilities in `class=`. `css=` compiles through the same plugin (not a `data-c` hash).
|
|
748
|
+
|
|
749
|
+
## Fonts and images
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
import { FontCSS } from "@lovrozagar/flare/fonts";
|
|
753
|
+
import { inter } from "@lovrozagar/flare/fonts/inter";
|
|
754
|
+
import { Image } from "@lovrozagar/flare/image";
|
|
755
|
+
import hero from "../assets/hero.jpg";
|
|
756
|
+
|
|
757
|
+
<FontCSS family="Inter" />
|
|
758
|
+
<Image src={hero} alt="Hero" widths={[400, 800, 1200]} placeholder={false} />
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`flare font add` writes `public/fonts/` + subset CSS. Fallback metrics (`size-adjust`) reduce CLS.
|
|
762
|
+
|
|
763
|
+
`Image` emits `srcset`, optional blur placeholder. The Vite image plugin rewrites imports. `configureImage({ ... })` sets app-wide defaults. Typed imports look like `hero.d.jpg.ts` next to the file.
|
|
764
|
+
|
|
765
|
+
## i18n, theme, direction
|
|
766
|
+
|
|
767
|
+
```ts
|
|
768
|
+
createRouter({
|
|
769
|
+
locale: { defaultLocale: "en", locales: ["en", "hr", "fr"], paramName: "locale" },
|
|
770
|
+
theme: { defaultTheme: "system" },
|
|
771
|
+
direction: { defaultDir: "ltr" },
|
|
772
|
+
});
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Put the blocking scripts in the root `<head>` so first paint is correct:
|
|
776
|
+
|
|
777
|
+
```tsx
|
|
778
|
+
import { LocaleScript, LocaleProvider } from "@lovrozagar/flare/locale";
|
|
779
|
+
import { ThemeScript, ThemeProvider } from "@lovrozagar/flare/theme";
|
|
780
|
+
import { DirectionScript } from "@lovrozagar/flare/direction";
|
|
781
|
+
|
|
782
|
+
<head>
|
|
783
|
+
<LocaleScript />
|
|
784
|
+
<ThemeScript />
|
|
785
|
+
<DirectionScript />
|
|
786
|
+
</head>;
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
- **Locale** — optional `[[locale]]` segment or prefix. Cookie `flare.locale` + `Accept-Language`. Default locale is stripped (`/en/about` → `/about`). Playwright / bot UAs skip Set-Cookie (`isbot`). Prefetch (`x-p: 1`) never writes the cookie.
|
|
790
|
+
- **Theme** — `data-theme`, system preference, `localStorage`.
|
|
791
|
+
- **Direction** — `dir` / `data-dir`.
|
|
792
|
+
|
|
793
|
+
Copy (separate from routing locale):
|
|
794
|
+
|
|
795
|
+
```ts
|
|
796
|
+
import { createTranslations, formatMessage } from "@lovrozagar/flare/i18n";
|
|
797
|
+
|
|
798
|
+
const translations = createTranslations({
|
|
799
|
+
common: {
|
|
800
|
+
en: () => import("./en/common"),
|
|
801
|
+
hr: () => import("./hr/common"),
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const dict = await translations.load("en", ["common"]);
|
|
806
|
+
formatMessage(dict.common.hello, { name: "Flare" });
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
`useLoaderT({ from })` / `usePreloaderT({ from })` format messages stored on loader / preloader data.
|
|
810
|
+
|
|
811
|
+
## Middleware
|
|
812
|
+
|
|
813
|
+
```ts
|
|
814
|
+
import type { FlareMiddleware } from "@lovrozagar/flare/middleware";
|
|
815
|
+
import { onPage, virtualPath } from "@lovrozagar/flare/middleware";
|
|
816
|
+
import { i18n } from "@lovrozagar/flare/middleware/i18n";
|
|
817
|
+
import { keepalive } from "@lovrozagar/flare/middleware/keepalive";
|
|
818
|
+
import { staticAssets } from "@lovrozagar/flare/middleware/static-assets";
|
|
819
|
+
import { apiProxy } from "@lovrozagar/flare/middleware/api-proxy";
|
|
820
|
+
import { cdnProxy } from "@lovrozagar/flare/middleware/cdn-proxy";
|
|
821
|
+
import { markdownNegotiation } from "@lovrozagar/flare/middleware/markdown-negotiation";
|
|
822
|
+
|
|
823
|
+
const timing: FlareMiddleware = async (ctx) => {
|
|
824
|
+
const start = Date.now();
|
|
825
|
+
ctx.onResponse((response) => {
|
|
826
|
+
const headers = new Headers(response.headers);
|
|
827
|
+
headers.set("x-timing", `${Date.now() - start}ms`);
|
|
828
|
+
return new Response(response.body, { headers, status: response.status });
|
|
829
|
+
});
|
|
830
|
+
return ctx.next();
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
createServer(router)
|
|
834
|
+
.use(i18n({ locales: ["en", "hr"], defaultLocale: "en" }))
|
|
835
|
+
.use(onPage(timing))
|
|
836
|
+
.use(virtualPath("_root_/about"), timing)
|
|
837
|
+
.use(keepalive())
|
|
838
|
+
.use(staticAssets())
|
|
839
|
+
.use("/api/*", apiProxy({ target: "https://api.example.com" }))
|
|
840
|
+
.use(cdnProxy())
|
|
841
|
+
.use(markdownNegotiation());
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
| Result | Meaning |
|
|
845
|
+
| ----------------------- | ------------------------------------------------------- |
|
|
846
|
+
| `ctx.next()` | Continue |
|
|
847
|
+
| `ctx.respond(response)` | Stop and return that response (still runs `onResponse`) |
|
|
848
|
+
| `ctx.bypass(response)` | Return as-is |
|
|
849
|
+
|
|
850
|
+
`requestType` is `"page"` \| `"server-fn"` \| `"mount"` \| `"internal"`. Per-route: `.middleware(fn)`.
|
|
851
|
+
|
|
852
|
+
## Mount
|
|
853
|
+
|
|
854
|
+
In-process island that is not a Flare page:
|
|
855
|
+
|
|
856
|
+
```ts
|
|
857
|
+
createServer(router).mount("/api", (request, env, ctx) => {
|
|
858
|
+
ctx.waitUntil(log(request));
|
|
859
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
860
|
+
headers: { "content-type": "application/json" },
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
Or pass an object with `{ fetch }`. Also exported as `mount` from `@lovrozagar/flare/mount`.
|
|
866
|
+
|
|
867
|
+
## Security
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
createServer(router).security(({ nonce }) => ({
|
|
871
|
+
"Content-Security-Policy": {
|
|
872
|
+
"default-src": ["'self'"],
|
|
873
|
+
"script-src": ["'self'", `'nonce-${nonce}'`],
|
|
874
|
+
},
|
|
875
|
+
"X-Frame-Options": "DENY",
|
|
876
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
877
|
+
"Strict-Transport-Security": "max-age=31536000",
|
|
878
|
+
}));
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
Defaults: `nosniff`, CSP (dev `unsafe-inline`; prod nonce on HTML), HSTS in prod (skipped in dev). Set a header to `false` to omit it.
|
|
882
|
+
|
|
883
|
+
## Store and revalidation
|
|
884
|
+
|
|
885
|
+
```ts
|
|
886
|
+
import type { FlareStore, FlareStoreEntry } from "@lovrozagar/flare/store";
|
|
887
|
+
import { createFilesystemStore } from "@lovrozagar/flare/store-filesystem";
|
|
888
|
+
import { createRevalidateFn } from "@lovrozagar/flare/revalidation";
|
|
889
|
+
|
|
890
|
+
const store: FlareStore = createFilesystemStore(".flare/cache");
|
|
891
|
+
/* or implement { get, set, delete, deleteByTags } yourself */
|
|
892
|
+
|
|
893
|
+
createServer(router).cache({ store, cdnPurgeAdapter });
|
|
894
|
+
|
|
895
|
+
const revalidate = createRevalidateFn({ store, cdnPurgeAdapter });
|
|
896
|
+
await revalidate({ tags: ["posts"], keys: ["static:/about"], tiers: ["ssr", "cdn"] });
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
HTTP purge: `POST` with header `x-revalidation-secret` (set the secret on the handler cache config). Load prerendered artifacts with `loadPrerenderArtifacts(dir, store)` from `@lovrozagar/flare/prerender`.
|
|
900
|
+
|
|
901
|
+
## Query
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
import { getQueryClient } from "./query-client";
|
|
905
|
+
import { useSuspenseQuery } from "@lovrozagar/flare/suspense-query";
|
|
906
|
+
import { BroadcastProvider } from "@lovrozagar/flare/broadcast";
|
|
907
|
+
|
|
908
|
+
createRouter({ queryClientGetter: getQueryClient });
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
SSR dehydrates into the stream (`__flare_qc` / `t:"q"`). `useSuspenseQuery` suspends until the dehydrated entry is ready. Optional `BroadcastProvider` invalidates across tabs.
|
|
912
|
+
|
|
913
|
+
```ts
|
|
914
|
+
import { createQueryClientGetter } from "@lovrozagar/flare/query-client";
|
|
915
|
+
|
|
916
|
+
export const getQueryClient = createQueryClientGetter(() => new QueryClient());
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
## Lazy
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
import { lazy, clientLazy } from "@lovrozagar/flare/lazy";
|
|
923
|
+
|
|
924
|
+
const Heavy = lazy({
|
|
925
|
+
loader: () => import("./heavy"),
|
|
926
|
+
pending: () => <p>Loading</p>,
|
|
927
|
+
error: (err) => <p>{err.message}</p>,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const BrowserOnly = clientLazy({
|
|
931
|
+
loader: () => import("./chart"),
|
|
932
|
+
});
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
`lazy()` works on server and client. `clientLazy()` is browser-only (no SSR). Prefetch can preload chunks.
|
|
936
|
+
|
|
937
|
+
## Service worker
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
flare({
|
|
941
|
+
serviceWorker: { offlineFallback: "/offline" },
|
|
942
|
+
});
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
Dev `sw.js` uses `skipWaiting` + `clients.claim`. The offline route is your page (`/offline`). Disable with `serviceWorker: false`.
|
|
946
|
+
|
|
947
|
+
## Sitemap and search engines
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
createServer(router).sitemap({
|
|
951
|
+
origin: "https://example.com",
|
|
952
|
+
changefreq: "weekly",
|
|
953
|
+
exclude: ["/admin/*"],
|
|
954
|
+
additionalEntries: [{ loc: "https://example.com/extra", priority: 0.3 }],
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
Or `generateSitemap(defs, config)` from `@lovrozagar/flare/sitemap`.
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
import { submitIndexNow, indexNowVerification } from "@lovrozagar/flare/search-engine";
|
|
962
|
+
import { submitSitemapToGoogle, notifyGoogleIndexing } from "@lovrozagar/flare/search-engine";
|
|
963
|
+
import { submitUrlsToBing } from "@lovrozagar/flare/search-engine";
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
IndexNow, Google Indexing / sitemap ping, Bing URL submit. Credentials stay on the server.
|
|
967
|
+
|
|
968
|
+
## Tracing
|
|
969
|
+
|
|
970
|
+
```ts
|
|
971
|
+
import { createTimingTracer, createOtelTracer, noopTracer } from "@lovrozagar/flare/tracing";
|
|
972
|
+
|
|
973
|
+
createServer(router).tracing({
|
|
974
|
+
timing: true,
|
|
975
|
+
tracer: createTimingTracer(),
|
|
976
|
+
});
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
`timing: true` emits `Server-Timing`. Pass an OTel-compatible tracer for spans.
|
|
980
|
+
|
|
981
|
+
## Testing
|
|
982
|
+
|
|
983
|
+
```ts
|
|
984
|
+
import { FlarePage, assertFlareStateShape, isHydrationError } from "@lovrozagar/flare/testing";
|
|
985
|
+
|
|
986
|
+
const flare = new FlarePage(page);
|
|
987
|
+
await flare.goto("/");
|
|
988
|
+
await flare.assertHydrated();
|
|
989
|
+
assertFlareStateShape(await flare.state());
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
Playwright helpers: hydration, FlareState shape, console-error filters. Product e2e also has app-local helpers under `e2e/apps/product/tests/e2e/helpers.ts`.
|
|
993
|
+
|
|
994
|
+
## NDJSON protocol
|
|
995
|
+
|
|
996
|
+
SPA / prefetch / data requests send `x-d: 1`. Prefetch also sends `x-p: 1`. Stale match skip uses `x-m`.
|
|
997
|
+
|
|
998
|
+
| `t` | Meaning |
|
|
999
|
+
| --- | --------------- |
|
|
1000
|
+
| `l` | Loader payload |
|
|
1001
|
+
| `c` | Deferred chunk |
|
|
1002
|
+
| `h` | Head |
|
|
1003
|
+
| `r` | Redirect |
|
|
1004
|
+
| `q` | Query dehydrate |
|
|
1005
|
+
|
|
1006
|
+
There is no JSON-RPC twin. HTML is `renderToStream`. Hydration reads `self.flare` (FlareState): `p` pathname, `r` params/search, `m` matches, `c` serializable router config.
|
|
1007
|
+
|
|
1008
|
+
## Duration strings
|
|
1009
|
+
|
|
1010
|
+
Anywhere a duration is accepted: number (ms) or `"30s"` / `"5m"` / `"1h"` / `"1d"`.
|
|
1011
|
+
|
|
1012
|
+
## Plugin
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
import { defineConfig } from "vite";
|
|
8
1016
|
import { flare } from "@lovrozagar/flare/plugins";
|
|
1017
|
+
|
|
1018
|
+
export default defineConfig({
|
|
1019
|
+
plugins: [
|
|
1020
|
+
flare({
|
|
1021
|
+
codegen: { fsVirtualPaths: false },
|
|
1022
|
+
dev: { cdnCache: false, dashboard: true, serverTiming: true },
|
|
1023
|
+
prerender: true,
|
|
1024
|
+
serviceWorker: { offlineFallback: "/offline" },
|
|
1025
|
+
sx: { tw: true },
|
|
1026
|
+
image: { quality: 80, widths: [400, 800, 1200] },
|
|
1027
|
+
assetsBase: "/assets",
|
|
1028
|
+
alias: { "@": "/src" },
|
|
1029
|
+
port: undefined,
|
|
1030
|
+
purge: false,
|
|
1031
|
+
logLevel: "info",
|
|
1032
|
+
}),
|
|
1033
|
+
],
|
|
1034
|
+
});
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
| Option | Default | Meaning |
|
|
1038
|
+
| -------------------------------------- | ------------------------------------------- | ------------------------------------------------------------ |
|
|
1039
|
+
| `codegen.fsVirtualPaths` | `true` | Suffix files vs string `createPage` |
|
|
1040
|
+
| `codegen.routesFilePath` | `src/_gen/routes.gen.ts` | |
|
|
1041
|
+
| `codegen.typesFilePath` | `src/_gen/types.gen.d.ts` | |
|
|
1042
|
+
| `dev.cdnCache` | `true` | Disk CDN emulator |
|
|
1043
|
+
| `dev.dashboard` | `true` | `/__flare` (node-only) |
|
|
1044
|
+
| `dev.serverTiming` | `true` | `Server-Timing` |
|
|
1045
|
+
| `dev.staticCache` | `true` | |
|
|
1046
|
+
| `prerender` | off | SSG/ISR emit |
|
|
1047
|
+
| `purge` | off | Dead CSS / `data-testid` strip |
|
|
1048
|
+
| `serviceWorker` | off | `sw.js` |
|
|
1049
|
+
| `sx.tw` | compile `class=` Tailwind | |
|
|
1050
|
+
| `image.quality` / `widths` / `exclude` | image pipeline | |
|
|
1051
|
+
| `assetsBase` | `"/assets"` | Must start with `/`, no trailing `/` (`"/"` = root-relative) |
|
|
1052
|
+
| `entry.client` / `entry.server` | `src/client`, `src/server` | |
|
|
1053
|
+
| `ignorePrefix` | extra ignored folder names | |
|
|
1054
|
+
| `port` | Vite `server.port` (overrides CLI `--port`) | |
|
|
1055
|
+
| `alias` | Vite alias | |
|
|
1056
|
+
| `solid` | passed to `vite-plugin-solid` | |
|
|
1057
|
+
|
|
1058
|
+
`dev: false` turns every `dev.*` flag off. Dev dashboard is `@node-only` in e2e (not on Workers).
|
|
1059
|
+
|
|
1060
|
+
Do **not** set `flare({ port: 3000 })` in e2e apps — it steals Playwright’s `--port`.
|
|
1061
|
+
|
|
1062
|
+
## Package exports
|
|
1063
|
+
|
|
1064
|
+
Import features from their path.
|
|
1065
|
+
|
|
1066
|
+
| Export | You get |
|
|
1067
|
+
| --------------------------------------- | --------------------------------------- |
|
|
1068
|
+
| `@lovrozagar/flare` | `createRouter`, hooks, `navigate` |
|
|
1069
|
+
| `@lovrozagar/flare/router` | `createRouter` |
|
|
1070
|
+
| `@lovrozagar/flare/page` | `createPage` |
|
|
1071
|
+
| `@lovrozagar/flare/layout` | `createLayout` |
|
|
1072
|
+
| `@lovrozagar/flare/root-layout` | `createRootLayout` |
|
|
1073
|
+
| `@lovrozagar/flare/path-segment` | `createPathSegment` |
|
|
1074
|
+
| `@lovrozagar/flare/link` | `Link` |
|
|
1075
|
+
| `@lovrozagar/flare/outlet` | `Outlet` |
|
|
1076
|
+
| `@lovrozagar/flare/hydrate` | `hydrate` |
|
|
1077
|
+
| `@lovrozagar/flare/client` | `createClient` |
|
|
1078
|
+
| `@lovrozagar/flare/await` | `<Await>` |
|
|
1079
|
+
| `@lovrozagar/flare/form` | `Form`, `FieldError` |
|
|
1080
|
+
| `@lovrozagar/flare/server-fn` | `createServerFn` |
|
|
1081
|
+
| `@lovrozagar/flare/server-fn-query` | server fn ↔ Query |
|
|
1082
|
+
| `@lovrozagar/flare/plugins` | `flare()` |
|
|
1083
|
+
| `@lovrozagar/flare/styles` | `styles`, `cn`, `compileSx` |
|
|
1084
|
+
| `@lovrozagar/flare/fonts` | `FontCSS`, `createFont` |
|
|
1085
|
+
| `@lovrozagar/flare/fonts/<family>` | `import { inter } from "…/fonts/inter"` |
|
|
1086
|
+
| `@lovrozagar/flare/image` | `Image`, `configureImage` |
|
|
1087
|
+
| `@lovrozagar/flare/theme` | `ThemeScript`, `ThemeProvider` |
|
|
1088
|
+
| `@lovrozagar/flare/direction` | `DirectionScript` |
|
|
1089
|
+
| `@lovrozagar/flare/locale` | `LocaleScript`, `LocaleProvider` |
|
|
1090
|
+
| `@lovrozagar/flare/i18n` | `createTranslations`, `formatMessage` |
|
|
1091
|
+
| `@lovrozagar/flare/middleware` | `onPage`, `virtualPath`, types |
|
|
1092
|
+
| `@lovrozagar/flare/middleware/*` | builtins |
|
|
1093
|
+
| `@lovrozagar/flare/errors` | `NotFoundError`, redirects, auth errors |
|
|
1094
|
+
| `@lovrozagar/flare/security` | `SecurityConfig` |
|
|
1095
|
+
| `@lovrozagar/flare/revalidation` | `createRevalidateFn` |
|
|
1096
|
+
| `@lovrozagar/flare/store` | `FlareStore` |
|
|
1097
|
+
| `@lovrozagar/flare/store-filesystem` | disk store |
|
|
1098
|
+
| `@lovrozagar/flare/query-client` | `createQueryClientGetter` |
|
|
1099
|
+
| `@lovrozagar/flare/suspense-query` | `useSuspenseQuery` |
|
|
1100
|
+
| `@lovrozagar/flare/broadcast` | cross-tab |
|
|
1101
|
+
| `@lovrozagar/flare/lazy` | `lazy`, `clientLazy` |
|
|
1102
|
+
| `@lovrozagar/flare/server` | `createServer` |
|
|
1103
|
+
| `@lovrozagar/flare/server-context` | ALS, `background` |
|
|
1104
|
+
| `@lovrozagar/flare/server-only` | `createServerOnlyFn` |
|
|
1105
|
+
| `@lovrozagar/flare/client-only` | `createClientOnlyFn` |
|
|
1106
|
+
| `@lovrozagar/flare/isomorphic` | `createIsomorphicFn` |
|
|
1107
|
+
| `@lovrozagar/flare/testing` | Playwright helpers |
|
|
1108
|
+
| `@lovrozagar/flare/sitemap` | sitemap XML |
|
|
1109
|
+
| `@lovrozagar/flare/search-engine` | IndexNow / Google / Bing |
|
|
1110
|
+
| `@lovrozagar/flare/rewrite` | `LocationRewrite` |
|
|
1111
|
+
| `@lovrozagar/flare/mount` | `mount` |
|
|
1112
|
+
| `@lovrozagar/flare/intercept-outlet` | `InterceptOutlet` |
|
|
1113
|
+
| `@lovrozagar/flare/navigation-progress` | `<NavigationProgress>` |
|
|
1114
|
+
| `@lovrozagar/flare/reset-css` | `<ResetCSS>` |
|
|
1115
|
+
| `@lovrozagar/flare/view-transition-css` | `<ViewTransitionCSS>` |
|
|
1116
|
+
| `@lovrozagar/flare/prerender` | `loadPrerenderArtifacts` |
|
|
1117
|
+
| `@lovrozagar/flare/tracing` | timing / OTel |
|
|
1118
|
+
| `@lovrozagar/flare/validation` | `Validator`, `runValidator` |
|
|
1119
|
+
| `@lovrozagar/flare/codegen` | generated types |
|
|
1120
|
+
| `@lovrozagar/flare/generators` | `runGenerate` |
|
|
1121
|
+
| `@lovrozagar/flare/virtual-types` | `/// <reference types="…" />` |
|
|
1122
|
+
|
|
1123
|
+
## Repository layout
|
|
1124
|
+
|
|
1125
|
+
```
|
|
1126
|
+
packages/core/ published `@lovrozagar/flare` (src, tests, spec)
|
|
1127
|
+
packages/cli/ `flare` CLI (`@flare/cli`)
|
|
1128
|
+
e2e/apps/ product, demo, fs-routes, tauri
|
|
1129
|
+
e2e/{node,bun,workers,deno}/ env hosts + Playwright
|
|
1130
|
+
e2e/run-env.ts env × app runner
|
|
1131
|
+
benchmark/ flare vs Next vs TanStack
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
`e2e/apps/*` own the tests. Runtimes only listen. `FLARE_E2E_APP` and `FLARE_E2E_ENV` select app and host.
|
|
1135
|
+
|
|
1136
|
+
## Develop
|
|
1137
|
+
|
|
1138
|
+
Requires [Bun](https://bun.sh) 1.3+ and TypeScript 7.
|
|
1139
|
+
|
|
1140
|
+
```bash
|
|
1141
|
+
bun install
|
|
1142
|
+
bun run test # core unit
|
|
1143
|
+
bun run test:cli # CLI unit
|
|
1144
|
+
bun run test:all # unit + all e2e apps on node
|
|
1145
|
+
bun run test:all -- --env bun
|
|
1146
|
+
bun run typecheck
|
|
1147
|
+
bun run typecheck:consumers
|
|
1148
|
+
bun run lint
|
|
1149
|
+
bun run fmt:check
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
GitHub Actions (`.github/workflows/ci.yml`): `test` runs typecheck, fmt, lint, unit. `e2e` is a matrix of node / bun / workers / deno.
|
|
1153
|
+
|
|
1154
|
+
### Checks
|
|
1155
|
+
|
|
1156
|
+
TypeScript 7 `strict` plus [oxlint](https://oxc.rs/docs/guide/usage/linter.html) / [oxfmt](https://oxc.rs/docs/guide/usage/formatter.html):
|
|
1157
|
+
|
|
1158
|
+
```bash
|
|
1159
|
+
bun run typecheck
|
|
1160
|
+
bun run typecheck:consumers
|
|
1161
|
+
bun run lint
|
|
1162
|
+
bun run lint:fix
|
|
1163
|
+
bun run fmt
|
|
1164
|
+
bun run fmt:check
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
Config: `.oxlintrc.json`, `.oxfmtrc.jsonc`. Tabs, double quotes, `printWidth` 120, semicolons. Generated `_gen/` / `*.gen.ts` are ignored.
|
|
1168
|
+
|
|
1169
|
+
Do not weaken `strict` or add `as any` to make typecheck pass.
|
|
1170
|
+
|
|
1171
|
+
### Test matrix
|
|
1172
|
+
|
|
1173
|
+
| Command | What it proves |
|
|
1174
|
+
| -------------------------- | ------------------------------ |
|
|
1175
|
+
| `bun run test` | Core unit + integration |
|
|
1176
|
+
| `bun run test:cli` | CLI unit |
|
|
1177
|
+
| `bun run test:e2e` | Playwright, every app × node |
|
|
1178
|
+
| `bun run test:e2e:bun` | Same tests, Bun listen |
|
|
1179
|
+
| `bun run test:e2e:workers` | Same tests, local workerd |
|
|
1180
|
+
| `bun run test:e2e:deno` | Same tests, Deno |
|
|
1181
|
+
| `bun run test:e2e:firefox` | Same tests, Firefox (dev only) |
|
|
1182
|
+
| `bun run test:e2e:prod` | Node, `TEST_MODE=prod` |
|
|
1183
|
+
|
|
1184
|
+
```bash
|
|
1185
|
+
bun run test:e2e -- --app product
|
|
1186
|
+
bun run e2e/run-env.ts --env workers --app demo
|
|
1187
|
+
TEST_MODE=prod bun run test:e2e:workers
|
|
9
1188
|
```
|
|
10
1189
|
|
|
11
|
-
|
|
1190
|
+
| App | Covers |
|
|
1191
|
+
| ----------- | ----------------------------- |
|
|
1192
|
+
| `product` | Full framework surface |
|
|
1193
|
+
| `demo` | Locale / i18n chrome |
|
|
1194
|
+
| `fs-routes` | `fsVirtualPaths` suffix files |
|
|
1195
|
+
| `tauri` | Desktop Vite shell |
|
|
1196
|
+
|
|
1197
|
+
Runtimes: `e2e/node`, `e2e/bun`, `e2e/workers`, `e2e/deno`. Firefox uses the node host.
|
|
1198
|
+
|
|
1199
|
+
### Bench
|
|
1200
|
+
|
|
1201
|
+
`benchmark/` compares Flare, Next, and TanStack. Numbers: [`benchmark/RESULTS.md`](benchmark/RESULTS.md).
|
|
1202
|
+
|
|
1203
|
+
```bash
|
|
1204
|
+
bun run --filter @lovrozagar/flare bench
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
## License
|
|
1208
|
+
|
|
1209
|
+
MIT. Copyright (c) 2026 Lovro Žagar.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/flare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flare",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"streaming",
|
|
13
13
|
"vite"
|
|
14
14
|
],
|
|
15
|
+
"homepage": "https://github.com/lovrozagar/flare",
|
|
15
16
|
"license": "MIT",
|
|
16
17
|
"repository": {
|
|
17
18
|
"type": "git",
|
|
@@ -105,7 +106,7 @@
|
|
|
105
106
|
"@capsizecss/metrics": "catalog:",
|
|
106
107
|
"@formatjs/intl-localematcher": "catalog:",
|
|
107
108
|
"@solidjs/testing-library": "catalog:",
|
|
108
|
-
"@tanstack/query-broadcast-client-experimental": "
|
|
109
|
+
"@tanstack/query-broadcast-client-experimental": "catalog:",
|
|
109
110
|
"@tanstack/solid-query": "catalog:",
|
|
110
111
|
"@types/negotiator": "catalog:",
|
|
111
112
|
"isbot": "catalog:",
|
|
@@ -139,8 +140,32 @@
|
|
|
139
140
|
"vite-plugin-solid": ">=2.0.0"
|
|
140
141
|
},
|
|
141
142
|
"peerDependenciesMeta": {
|
|
143
|
+
"@formatjs/intl-localematcher": {
|
|
144
|
+
"optional": true
|
|
145
|
+
},
|
|
146
|
+
"@tanstack/query-broadcast-client-experimental": {
|
|
147
|
+
"optional": true
|
|
148
|
+
},
|
|
149
|
+
"@tanstack/solid-query": {
|
|
150
|
+
"optional": true
|
|
151
|
+
},
|
|
152
|
+
"isbot": {
|
|
153
|
+
"optional": true
|
|
154
|
+
},
|
|
155
|
+
"negotiator": {
|
|
156
|
+
"optional": true
|
|
157
|
+
},
|
|
142
158
|
"node-html-markdown": {
|
|
143
159
|
"optional": true
|
|
160
|
+
},
|
|
161
|
+
"oxc-parser": {
|
|
162
|
+
"optional": true
|
|
163
|
+
},
|
|
164
|
+
"oxc-resolver": {
|
|
165
|
+
"optional": true
|
|
166
|
+
},
|
|
167
|
+
"sharp": {
|
|
168
|
+
"optional": true
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
}
|