@lovrozagar/flare 0.1.2 → 0.2.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 +42 -19
- package/package.json +8 -6
- package/src/broadcast/index.ts +1 -1
- package/src/broadcast/provider.tsx +2 -2
- package/src/caches/index.ts +26 -0
- package/src/components/await.tsx +36 -39
- package/src/components/dev-error-overlay.tsx +8 -9
- package/src/components/direction-script.tsx +2 -2
- package/src/components/locale-script.tsx +2 -2
- package/src/components/reset-css.tsx +1 -1
- package/src/components/ssr-context.tsx +7 -14
- package/src/components/theme-script.tsx +2 -2
- package/src/components/view-transition-css.tsx +1 -1
- package/src/defer/index.ts +22 -0
- package/src/direction/index.tsx +61 -36
- package/src/errors/index.ts +45 -11
- package/src/fonts/component.tsx +2 -1
- package/src/form/index.tsx +18 -12
- package/src/generators/index.ts +73 -30
- package/src/head-client/index.ts +86 -29
- package/src/history/index.ts +6 -0
- package/src/hydrate/index.tsx +90 -60
- package/src/hydration/index.ts +26 -6
- package/src/i18n/index.ts +11 -2
- package/src/image/index.tsx +7 -4
- package/src/intercept-outlet/index.tsx +6 -2
- package/src/lazy/index.tsx +50 -50
- package/src/link/index.tsx +97 -58
- package/src/loader-pipeline/index.ts +119 -38
- package/src/locale/cookie.ts +15 -0
- package/src/locale/index.tsx +24 -35
- package/src/locale.ts +1 -0
- package/src/middleware/builtins/api-proxy.ts +58 -4
- package/src/middleware/builtins/cdn-proxy.ts +24 -3
- package/src/middleware/builtins/i18n.ts +23 -25
- package/src/middleware/builtins/keepalive.ts +2 -3
- package/src/mount/index.ts +8 -1
- package/src/navigation/index.ts +575 -129
- package/src/navigation/types.ts +11 -1
- package/src/navigation-progress/index.tsx +17 -16
- package/src/ndjson-client/index.ts +42 -17
- package/src/ndjson-server/index.ts +78 -29
- package/src/outlet/index.tsx +192 -97
- package/src/outlet/types.ts +9 -1
- package/src/plugins/dev-cdn-cache.ts +18 -5
- package/src/plugins/dev-dashboard/client.tsx +130 -118
- package/src/plugins/dev-prerender.ts +6 -1
- package/src/plugins/dev-server.ts +12 -3
- package/src/plugins/image-plugin.ts +18 -4
- package/src/plugins/index.ts +10 -7
- package/src/plugins/prerender-plugin.ts +3 -3
- package/src/plugins/server-fn.ts +15 -20
- package/src/plugins/solid-jsx.d.ts +3 -3
- package/src/plugins/sx-ast/atomic.ts +5 -5
- package/src/plugins/sx-ast/index.ts +9 -0
- package/src/plugins/sx.d.ts +4 -5
- package/src/plugins/virtual.ts +11 -6
- package/src/prerender/index.ts +68 -32
- package/src/protocol.ts +65 -0
- package/src/query-client/index.tsx +73 -14
- package/src/rewrite/index.ts +10 -4
- package/src/route-builder/create-layout.ts +1 -1
- package/src/route-builder/create-root-layout.ts +1 -1
- package/src/route-builder/types.ts +5 -1
- package/src/router-primitives/types.ts +2 -0
- package/src/security/index.ts +41 -5
- package/src/server/index.ts +1 -2
- package/src/server-fn/get-input.ts +90 -0
- package/src/server-fn/http-error.ts +13 -0
- package/src/server-fn/index.ts +94 -70
- package/src/server-fn-query.ts +9 -17
- package/src/server-handler/etag.ts +4 -2
- package/src/server-handler/headers.ts +1 -2
- package/src/server-handler/index.ts +166 -58
- package/src/server-handler/types.ts +2 -0
- package/src/service-worker/template.ts +10 -42
- package/src/ssr/head-prefix.ts +2 -1
- package/src/ssr/hoist-head-markers.ts +25 -0
- package/src/ssr/index.tsx +109 -92
- package/src/state-parser/index.ts +38 -26
- package/src/styles/index.ts +89 -60
- package/src/testing/index.ts +8 -3
- package/src/theme/index.tsx +35 -32
- package/src/tracing/timing.ts +6 -1
- package/src/url/index.ts +9 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Flare
|
|
2
2
|
|
|
3
|
-
Solid + Vite meta-framework. You declare pages and layouts with a typed builder. The server streams HTML (`renderToStream`). Later navigations speak NDJSON (`
|
|
3
|
+
Solid + Vite meta-framework. You declare pages and layouts with a typed builder. The server streams HTML (`renderToStream`). Later navigations speak NDJSON (`flare-data: 1`). Prefetch, ISR, deferred loaders, and forms reuse that protocol.
|
|
4
4
|
|
|
5
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
6
|
|
|
@@ -123,7 +123,7 @@ npm add @lovrozagar/flare
|
|
|
123
123
|
pnpm add @lovrozagar/flare
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
Peers: `solid-js`,
|
|
126
|
+
Peers: `solid-js`, `@solidjs/web`, `@solidjs/vite-plugin`, `vite`. 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
127
|
|
|
128
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
129
|
|
|
@@ -132,7 +132,7 @@ The CLI is a workspace package (`@flare/cli`, binary `flare`). In this repo it i
|
|
|
132
132
|
```bash
|
|
133
133
|
mkdir my-app && cd my-app
|
|
134
134
|
bun init -y
|
|
135
|
-
bun add @lovrozagar/flare solid-js vite vite-plugin
|
|
135
|
+
bun add @lovrozagar/flare solid-js @solidjs/web vite @solidjs/vite-plugin
|
|
136
136
|
flare init
|
|
137
137
|
bun run dev
|
|
138
138
|
```
|
|
@@ -786,7 +786,7 @@ import { DirectionScript } from "@lovrozagar/flare/direction";
|
|
|
786
786
|
</head>;
|
|
787
787
|
```
|
|
788
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 (`
|
|
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 (`flare-prefetch: 1`) never writes the cookie.
|
|
790
790
|
- **Theme** — `data-theme`, system preference, `localStorage`.
|
|
791
791
|
- **Direction** — `dir` / `data-dir`.
|
|
792
792
|
|
|
@@ -908,7 +908,7 @@ import { BroadcastProvider } from "@lovrozagar/flare/broadcast";
|
|
|
908
908
|
createRouter({ queryClientGetter: getQueryClient });
|
|
909
909
|
```
|
|
910
910
|
|
|
911
|
-
SSR dehydrates into the stream (`
|
|
911
|
+
SSR dehydrates into the stream (`__flare_queries` / `t:"q"`). `useSuspenseQuery` suspends until the dehydrated entry is ready. Optional `BroadcastProvider` invalidates across tabs.
|
|
912
912
|
|
|
913
913
|
```ts
|
|
914
914
|
import { createQueryClientGetter } from "@lovrozagar/flare/query-client";
|
|
@@ -993,7 +993,28 @@ Playwright helpers: hydration, FlareState shape, console-error filters. Product
|
|
|
993
993
|
|
|
994
994
|
## NDJSON protocol
|
|
995
995
|
|
|
996
|
-
SPA / prefetch / data requests send `
|
|
996
|
+
SPA / prefetch / data requests send `flare-data: 1`. Prefetch also sends `flare-prefetch: 1`. Stale match skip uses `flare-stale`. Flag value is always `"1"`. Headers are lowercase (HTTP/2 / Node). NDJSON `t` codes and FlareState keys stay short.
|
|
997
|
+
|
|
998
|
+
| Request | Meaning |
|
|
999
|
+
| ----------------- | ------------------------------------------------- |
|
|
1000
|
+
| `flare-data` | NDJSON data request. `Vary` always includes this. |
|
|
1001
|
+
| `flare-prefetch` | Prefetch (no locale cookie commit) |
|
|
1002
|
+
| `flare-stale` | Comma-separated match ids the client already has |
|
|
1003
|
+
| `flare-isr` | Internal ISR background re-render |
|
|
1004
|
+
| `flare-prerender` | Build-time prerender fetch |
|
|
1005
|
+
|
|
1006
|
+
| Response | Meaning |
|
|
1007
|
+
| -------------- | ------------------------ |
|
|
1008
|
+
| `flare-cache` | `HIT` / `MISS` / `STALE` |
|
|
1009
|
+
| `flare-render` | `ISR` / `SSG` / `SSR` |
|
|
1010
|
+
|
|
1011
|
+
| Path / HTML | Meaning |
|
|
1012
|
+
| ------------------------------- | ------------------------------------------ |
|
|
1013
|
+
| `/_flare/server-fn/{id}/{name}` | Server functions |
|
|
1014
|
+
| `flare_fn` | Hidden form field for no-JS POST |
|
|
1015
|
+
| `data-flare-hydrated` | Set on `<html>` after `solidHydrate` |
|
|
1016
|
+
| `__flare_defer` | Window queue for SSR deferred chunks |
|
|
1017
|
+
| `__flare_queries` | Window queue for SSR query-cache dehydrate |
|
|
997
1018
|
|
|
998
1019
|
| `t` | Meaning |
|
|
999
1020
|
| --- | --------------- |
|
|
@@ -1053,7 +1074,7 @@ export default defineConfig({
|
|
|
1053
1074
|
| `ignorePrefix` | extra ignored folder names | |
|
|
1054
1075
|
| `port` | Vite `server.port` (overrides CLI `--port`) | |
|
|
1055
1076
|
| `alias` | Vite alias | |
|
|
1056
|
-
| `solid` | passed to
|
|
1077
|
+
| `solid` | passed to `@solidjs/vite-plugin` | `ssr: true` is forced; `start: true` is rejected |
|
|
1057
1078
|
|
|
1058
1079
|
`dev: false` turns every `dev.*` flag off. Dev dashboard is `@node-only` in e2e (not on Workers).
|
|
1059
1080
|
|
|
@@ -1141,7 +1162,7 @@ Requires [Bun](https://bun.sh) 1.3+ and TypeScript 7.
|
|
|
1141
1162
|
bun install
|
|
1142
1163
|
bun run test # core unit
|
|
1143
1164
|
bun run test:cli # CLI unit
|
|
1144
|
-
bun run test:all # unit +
|
|
1165
|
+
bun run test:all # unit + build + e2e apps on node (dev then prod)
|
|
1145
1166
|
bun run test:all -- --env bun
|
|
1146
1167
|
bun run typecheck
|
|
1147
1168
|
bun run typecheck:consumers
|
|
@@ -1149,7 +1170,7 @@ bun run lint
|
|
|
1149
1170
|
bun run fmt:check
|
|
1150
1171
|
```
|
|
1151
1172
|
|
|
1152
|
-
GitHub Actions (`.github/workflows/ci.yml`): `test` runs typecheck, fmt, lint, unit. `e2e` is a matrix of node / bun / workers / deno.
|
|
1173
|
+
GitHub Actions (`.github/workflows/ci.yml`): `test` runs typecheck, fmt, lint, unit. `e2e` is a matrix of node / bun / workers / deno, each in Vite **dev** and (except deno/firefox) **prod** (`vite preview`).
|
|
1153
1174
|
|
|
1154
1175
|
Release: bump `packages/core/package.json`, commit, tag `vX.Y.Z` matching that version, and push the tag. `.github/workflows/release.yml` publishes `@lovrozagar/flare` to npm via GitHub Actions OIDC (no npm token) and opens the GitHub Release. The npm trusted publisher must name this file `release.yml`.
|
|
1155
1176
|
|
|
@@ -1172,16 +1193,18 @@ Do not weaken `strict` or add `as any` to make typecheck pass.
|
|
|
1172
1193
|
|
|
1173
1194
|
### Test matrix
|
|
1174
1195
|
|
|
1175
|
-
| Command
|
|
1176
|
-
|
|
|
1177
|
-
| `bun run test`
|
|
1178
|
-
| `bun run test:cli`
|
|
1179
|
-
| `bun run test:e2e`
|
|
1180
|
-
| `bun run test:e2e:
|
|
1181
|
-
| `bun run test:e2e:
|
|
1182
|
-
| `bun run test:e2e:
|
|
1183
|
-
| `bun run test:e2e:
|
|
1184
|
-
| `bun run test:e2e:prod`
|
|
1196
|
+
| Command | What it proves |
|
|
1197
|
+
| ------------------------------- | ------------------------------------------ |
|
|
1198
|
+
| `bun run test` | Core unit + integration |
|
|
1199
|
+
| `bun run test:cli` | CLI unit |
|
|
1200
|
+
| `bun run test:e2e` | Playwright, every app × node, Vite **dev** |
|
|
1201
|
+
| `bun run test:e2e:prod` | Same tests, `vite build` + **preview** |
|
|
1202
|
+
| `bun run test:e2e:bun` | Same tests, Bun listen (dev) |
|
|
1203
|
+
| `bun run test:e2e:bun:prod` | Bun listen, prod preview |
|
|
1204
|
+
| `bun run test:e2e:workers` | Same tests, local workerd (dev) |
|
|
1205
|
+
| `bun run test:e2e:workers:prod` | workerd, prod preview |
|
|
1206
|
+
| `bun run test:e2e:deno` | Same tests, Deno (dev) |
|
|
1207
|
+
| `bun run test:e2e:firefox` | Same tests, Firefox (**dev only**) |
|
|
1185
1208
|
|
|
1186
1209
|
```bash
|
|
1187
1210
|
bun run test:e2e -- --app product
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovrozagar/flare",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"flare",
|
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
"@capsizecss/metrics": "catalog:",
|
|
108
108
|
"@formatjs/intl-localematcher": "catalog:",
|
|
109
109
|
"@solidjs/testing-library": "catalog:",
|
|
110
|
+
"@solidjs/vite-plugin": "catalog:",
|
|
111
|
+
"@solidjs/web": "catalog:",
|
|
110
112
|
"@tanstack/query-broadcast-client-experimental": "catalog:",
|
|
111
113
|
"@tanstack/solid-query": "catalog:",
|
|
112
114
|
"@types/negotiator": "catalog:",
|
|
@@ -121,14 +123,15 @@
|
|
|
121
123
|
"solid-js": "catalog:",
|
|
122
124
|
"typescript": "catalog:",
|
|
123
125
|
"vite": "catalog:",
|
|
124
|
-
"vite-plugin-solid": "catalog:",
|
|
125
126
|
"vitest": "catalog:"
|
|
126
127
|
},
|
|
127
128
|
"peerDependencies": {
|
|
128
129
|
"@babel/parser": ">=8.0.0",
|
|
129
130
|
"@formatjs/intl-localematcher": ">=0.6.0",
|
|
131
|
+
"@solidjs/vite-plugin": ">=3.0.0-next.0",
|
|
132
|
+
"@solidjs/web": ">=2.0.0-rc.0",
|
|
130
133
|
"@tanstack/query-broadcast-client-experimental": ">=5.0.0",
|
|
131
|
-
"@tanstack/solid-query": ">=
|
|
134
|
+
"@tanstack/solid-query": ">=6.0.0-rc.0",
|
|
132
135
|
"isbot": ">=5.0.0",
|
|
133
136
|
"magic-string": ">=0.30.0",
|
|
134
137
|
"negotiator": ">=1.0.0",
|
|
@@ -136,9 +139,8 @@
|
|
|
136
139
|
"oxc-parser": ">=0.139.0",
|
|
137
140
|
"oxc-resolver": ">=11.23.0",
|
|
138
141
|
"sharp": ">=0.33.0",
|
|
139
|
-
"solid-js": ">=
|
|
140
|
-
"vite": ">=8.0.0"
|
|
141
|
-
"vite-plugin-solid": ">=2.0.0"
|
|
142
|
+
"solid-js": ">=2.0.0-rc.0",
|
|
143
|
+
"vite": ">=8.0.0"
|
|
142
144
|
},
|
|
143
145
|
"peerDependenciesMeta": {
|
|
144
146
|
"@formatjs/intl-localematcher": {
|
package/src/broadcast/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function createBroadcastSignal<T>(key: string, initialValue: T): [() => T
|
|
|
42
42
|
|
|
43
43
|
let entry = registry.get(key);
|
|
44
44
|
if (!entry) {
|
|
45
|
-
const [value, setValue] = createSignal<T>
|
|
45
|
+
const [value, setValue] = createSignal(initialValue as Exclude<T, Function>);
|
|
46
46
|
entry = {
|
|
47
47
|
refCount: 0,
|
|
48
48
|
signal: [value, setValue as (v: unknown) => void],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type { InternalChannel } from "./channel.ts";
|
|
3
3
|
import { BroadcastCtx, noopChannel } from "./context.ts";
|
|
4
4
|
|
|
5
5
|
export function BroadcastProvider(props: { children: JSX.Element; value?: InternalChannel }): JSX.Element {
|
|
6
|
-
return <BroadcastCtx
|
|
6
|
+
return <BroadcastCtx value={props.value ?? noopChannel}>{props.children}</BroadcastCtx>;
|
|
7
7
|
}
|
package/src/caches/index.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import type { HeadConfig } from "../route-builder/types.ts";
|
|
2
2
|
|
|
3
|
+
export function resolveCacheTags(
|
|
4
|
+
cache:
|
|
5
|
+
| {
|
|
6
|
+
cdn?: { tags?: string[] | ((ctx: { params: Record<string, string | string[]> }) => string[]) } | false;
|
|
7
|
+
ssr?: { tags?: string[] | ((ctx: { params: Record<string, string | string[]> }) => string[]) } | false;
|
|
8
|
+
}
|
|
9
|
+
| undefined,
|
|
10
|
+
params: Record<string, string | string[]>,
|
|
11
|
+
): string[] | undefined {
|
|
12
|
+
if (!cache) return undefined;
|
|
13
|
+
const tags: string[] = [];
|
|
14
|
+
const push = (
|
|
15
|
+
src: { tags?: string[] | ((ctx: { params: Record<string, string | string[]> }) => string[]) } | false | undefined,
|
|
16
|
+
) => {
|
|
17
|
+
if (!src || !src.tags) return;
|
|
18
|
+
const list = typeof src.tags === "function" ? src.tags({ params }) : src.tags;
|
|
19
|
+
for (const t of list) {
|
|
20
|
+
if (typeof t === "string" && t.length > 0 && !tags.includes(t)) tags.push(t);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
push(cache.cdn);
|
|
24
|
+
push(cache.ssr);
|
|
25
|
+
return tags.length > 0 ? tags : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
export interface CachedMatch {
|
|
4
29
|
data: unknown;
|
|
5
30
|
error?: unknown;
|
|
31
|
+
gcTime?: number;
|
|
6
32
|
hasDeferred?: boolean;
|
|
7
33
|
headConfig?: HeadConfig;
|
|
8
34
|
invalid: boolean;
|
package/src/components/await.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createEffect, createMemo, createSignal, untrack } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
2
3
|
|
|
3
4
|
export interface Deferred<T> {
|
|
4
5
|
__deferred: true;
|
|
@@ -47,8 +48,10 @@ export function getResolvedError(value: unknown): Error | undefined {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export function Await<T>(props: AwaitProps<T>): JSX.Element {
|
|
50
|
-
|
|
51
|
-
const
|
|
51
|
+
/* Snapshot only — `createEffect` below tracks `props.promise` for updates. */
|
|
52
|
+
const initialPromise = untrack(() => props.promise);
|
|
53
|
+
const initialResolved = getResolvedValue(initialPromise);
|
|
54
|
+
const initialError = getResolvedError(initialPromise);
|
|
52
55
|
|
|
53
56
|
function computeInitialStatus(): AwaitStatus {
|
|
54
57
|
if (initialResolved !== undefined) return "success";
|
|
@@ -57,10 +60,10 @@ export function Await<T>(props: AwaitProps<T>): JSX.Element {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
const [status, setStatus] = createSignal<AwaitStatus>(computeInitialStatus());
|
|
60
|
-
const [data, setData] = createSignal<T | undefined>(initialResolved);
|
|
63
|
+
const [data, setData] = createSignal<T | undefined>(initialResolved as Exclude<T | undefined, Function>);
|
|
61
64
|
const [error, setError] = createSignal<Error | undefined>(initialError);
|
|
62
65
|
|
|
63
|
-
let currentPromise = getPromise(
|
|
66
|
+
let currentPromise = getPromise(initialPromise);
|
|
64
67
|
|
|
65
68
|
if (initialResolved === undefined && !initialError && currentPromise) {
|
|
66
69
|
trackPromise(currentPromise);
|
|
@@ -70,18 +73,14 @@ export function Await<T>(props: AwaitProps<T>): JSX.Element {
|
|
|
70
73
|
p.then(
|
|
71
74
|
(result) => {
|
|
72
75
|
if (currentPromise === p) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
setStatus("success");
|
|
76
|
-
});
|
|
76
|
+
setData(() => result);
|
|
77
|
+
setStatus("success");
|
|
77
78
|
}
|
|
78
79
|
},
|
|
79
80
|
(err: unknown) => {
|
|
80
81
|
if (currentPromise === p) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
setStatus("error");
|
|
84
|
-
});
|
|
82
|
+
setError(() => (err instanceof Error ? err : new Error(String(err))));
|
|
83
|
+
setStatus("error");
|
|
85
84
|
}
|
|
86
85
|
},
|
|
87
86
|
);
|
|
@@ -90,43 +89,41 @@ export function Await<T>(props: AwaitProps<T>): JSX.Element {
|
|
|
90
89
|
function reset(): void {
|
|
91
90
|
const p = getPromise(props.promise);
|
|
92
91
|
currentPromise = p;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
setError(undefined);
|
|
97
|
-
});
|
|
92
|
+
setStatus("pending");
|
|
93
|
+
setData(undefined);
|
|
94
|
+
setError(undefined);
|
|
98
95
|
if (p) trackPromise(p);
|
|
99
96
|
}
|
|
100
97
|
|
|
101
|
-
/* Watch for promise prop changes
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
/* Watch for promise prop changes. Wrap in an object so a Promise value is
|
|
99
|
+
not treated as an async computation (Solid 2 unwraps returned Promises). */
|
|
100
|
+
createEffect(
|
|
101
|
+
() => ({ promise: props.promise }),
|
|
102
|
+
({ promise }) => {
|
|
103
|
+
const newPromise = getPromise(promise);
|
|
104
|
+
if (newPromise === currentPromise) return;
|
|
105
|
+
currentPromise = newPromise;
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const resolved = getResolvedValue(promise);
|
|
108
|
+
const resolvedError = getResolvedError(promise);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
batch(() => {
|
|
110
|
+
if (resolved !== undefined) {
|
|
112
111
|
setData(() => resolved);
|
|
113
112
|
setStatus("success");
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (resolvedError) {
|
|
118
|
-
batch(() => {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (resolvedError) {
|
|
119
116
|
setError(() => resolvedError);
|
|
120
117
|
setStatus("error");
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
124
120
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
setStatus("pending");
|
|
122
|
+
if (newPromise) trackPromise(newPromise);
|
|
123
|
+
},
|
|
124
|
+
);
|
|
128
125
|
|
|
129
|
-
return (() => {
|
|
126
|
+
return createMemo(() => {
|
|
130
127
|
const s = status();
|
|
131
128
|
if (s === "success") {
|
|
132
129
|
return props.children(data() as T);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createSignal, For,
|
|
2
|
-
import {
|
|
1
|
+
import { createSignal, For, onSettled, Show } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import { Portal } from "@solidjs/web";
|
|
3
4
|
import { createDevErrorStore, type DevErrorStore } from "./index.ts";
|
|
4
5
|
|
|
5
6
|
let globalStore: DevErrorStore | undefined;
|
|
@@ -34,16 +35,14 @@ export function DevErrorOverlay(): JSX.Element {
|
|
|
34
35
|
refresh();
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
onSettled(() => {
|
|
38
39
|
if (typeof window === "undefined") return;
|
|
39
40
|
window.addEventListener("error", handleWindowError);
|
|
40
41
|
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
window.removeEventListener("error", handleWindowError);
|
|
46
|
-
window.removeEventListener("unhandledrejection", handleUnhandledRejection);
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener("error", handleWindowError);
|
|
44
|
+
window.removeEventListener("unhandledrejection", handleUnhandledRejection);
|
|
45
|
+
};
|
|
47
46
|
});
|
|
48
47
|
|
|
49
48
|
function dismiss(id: string): void {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
2
|
-
import { NoHydration } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import { NoHydration } from "@solidjs/web";
|
|
3
3
|
import type { DirectionConfig } from "../direction.ts";
|
|
4
4
|
import { getDirectionScript } from "../direction.ts";
|
|
5
5
|
import { useSSRContext } from "./ssr-context.tsx";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
2
|
-
import { NoHydration } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import { NoHydration } from "@solidjs/web";
|
|
3
3
|
import type { LocaleConfig } from "../locale/index.tsx";
|
|
4
4
|
import { getLocaleScript } from "../locale/index.tsx";
|
|
5
5
|
import { useSSRContext } from "./ssr-context.tsx";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createContext,
|
|
1
|
+
import { createContext, useContext } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
2
3
|
import type { DirectionConfig } from "../direction.ts";
|
|
3
4
|
import type { LocaleConfig } from "../locale/index.tsx";
|
|
4
5
|
import type { HeadConfig } from "../route-builder/types.ts";
|
|
@@ -15,23 +16,15 @@ export interface SSRContextValue {
|
|
|
15
16
|
theme?: ThemeConfig;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const SSRCtx = createContext<SSRContextValue>();
|
|
19
|
+
const SSRCtx = createContext<SSRContextValue | null>(null);
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(sharedConfig.context as Record<string, unknown>).flare = value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
21
|
+
/** @deprecated Wrap the tree with SSRContextProvider. Solid 2 has no sharedConfig.context. */
|
|
22
|
+
export function setSSRContext(_value: SSRContextValue): void {}
|
|
25
23
|
|
|
26
24
|
export function useSSRContext(): SSRContextValue | undefined {
|
|
27
|
-
|
|
28
|
-
if (sharedConfig.context) {
|
|
29
|
-
return (sharedConfig.context as Record<string, unknown>).flare as SSRContextValue | undefined;
|
|
30
|
-
}
|
|
31
|
-
/* Testing fallback: read from Solid context provider */
|
|
32
|
-
return useContext(SSRCtx);
|
|
25
|
+
return useContext(SSRCtx) ?? undefined;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
28
|
export function SSRContextProvider(props: { children: JSX.Element; value: SSRContextValue }): JSX.Element {
|
|
36
|
-
return <SSRCtx
|
|
29
|
+
return <SSRCtx value={props.value}>{props.children}</SSRCtx>;
|
|
37
30
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
2
|
-
import { NoHydration } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import { NoHydration } from "@solidjs/web";
|
|
3
3
|
import type { ThemeConfig } from "../theme.ts";
|
|
4
4
|
import { getThemeScript } from "../theme.ts";
|
|
5
5
|
import { useSSRContext } from "./ssr-context.tsx";
|
package/src/defer/index.ts
CHANGED
|
@@ -80,3 +80,25 @@ export function isDeferred(value: unknown): value is Deferred<unknown> {
|
|
|
80
80
|
(value as Record<string, unknown>).__deferred === true
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
/** True if `data` is or contains a deferred marker. Used to skip SSR store cache of unresolved streams. */
|
|
85
|
+
export function containsDeferred(data: unknown, seen?: WeakSet<object>): boolean {
|
|
86
|
+
if (data === null || data === undefined || typeof data !== "object") return false;
|
|
87
|
+
if (isDeferred(data)) return true;
|
|
88
|
+
|
|
89
|
+
const tracking = seen ?? new WeakSet<object>();
|
|
90
|
+
if (tracking.has(data as object)) return false;
|
|
91
|
+
tracking.add(data as object);
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(data)) {
|
|
94
|
+
for (const item of data) {
|
|
95
|
+
if (containsDeferred(item, tracking)) return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const value of Object.values(data as Record<string, unknown>)) {
|
|
101
|
+
if (containsDeferred(value, tracking)) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
package/src/direction/index.tsx
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
createEffect,
|
|
4
|
-
createSignal,
|
|
5
|
-
type JSX,
|
|
6
|
-
on,
|
|
7
|
-
onCleanup,
|
|
8
|
-
onMount,
|
|
9
|
-
sharedConfig,
|
|
10
|
-
useContext,
|
|
11
|
-
} from "solid-js";
|
|
1
|
+
import { createContext, createEffect, createSignal, onSettled, sharedConfig, untrack, useContext } from "solid-js";
|
|
2
|
+
import { isServer, type JSX } from "@solidjs/web";
|
|
12
3
|
|
|
13
4
|
export type Direction = "ltr" | "rtl";
|
|
14
5
|
|
|
@@ -60,14 +51,9 @@ export interface DirectionContextValue {
|
|
|
60
51
|
toggleDirection: () => void;
|
|
61
52
|
}
|
|
62
53
|
|
|
63
|
-
const DirectionCtx = createContext<DirectionContextValue>();
|
|
54
|
+
const DirectionCtx = createContext<DirectionContextValue | null>(null);
|
|
64
55
|
|
|
65
56
|
export function DirectionProvider(props: { children: JSX.Element; config?: DirectionConfig }): JSX.Element {
|
|
66
|
-
/* SSR: pass through children (context not needed on server, scripts handle direction) */
|
|
67
|
-
if (sharedConfig.context) {
|
|
68
|
-
return props.children;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
57
|
const cfg: Required<DirectionConfig> = {
|
|
72
58
|
attribute: props.config?.attribute ?? DEFAULT_CONFIG.attribute,
|
|
73
59
|
defaultDir: props.config?.defaultDir ?? DEFAULT_CONFIG.defaultDir,
|
|
@@ -75,9 +61,12 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
75
61
|
storageKey: props.config?.storageKey ?? DEFAULT_CONFIG.storageKey,
|
|
76
62
|
};
|
|
77
63
|
|
|
78
|
-
|
|
64
|
+
const hydrating = isServer || !!sharedConfig.hydrating;
|
|
65
|
+
|
|
66
|
+
/* During SSR + hydration, start from defaultDir so the tree matches.
|
|
67
|
+
DirectionScript already applied localStorage to <html> before first paint. */
|
|
79
68
|
let initial: Direction = cfg.defaultDir;
|
|
80
|
-
if (typeof localStorage !== "undefined") {
|
|
69
|
+
if (typeof localStorage !== "undefined" && !hydrating) {
|
|
81
70
|
try {
|
|
82
71
|
const stored = localStorage.getItem(cfg.storageKey);
|
|
83
72
|
if (stored === "ltr" || stored === "rtl") {
|
|
@@ -87,7 +76,7 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
87
76
|
/* noop */
|
|
88
77
|
}
|
|
89
78
|
}
|
|
90
|
-
if (initial === cfg.defaultDir && typeof document !== "undefined") {
|
|
79
|
+
if (initial === cfg.defaultDir && typeof document !== "undefined" && !hydrating) {
|
|
91
80
|
const domDir = document.documentElement.getAttribute("dir");
|
|
92
81
|
if (domDir === "ltr" || domDir === "rtl") {
|
|
93
82
|
initial = domDir;
|
|
@@ -96,17 +85,34 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
96
85
|
|
|
97
86
|
const [direction, setDirectionSignal] = createSignal<Direction>(initial);
|
|
98
87
|
|
|
99
|
-
|
|
100
|
-
createEffect(() => {
|
|
101
|
-
const dir = direction();
|
|
88
|
+
const applyToDocument = (dir: Direction): void => {
|
|
102
89
|
if (typeof document === "undefined") return;
|
|
103
90
|
const el = document.documentElement;
|
|
104
91
|
el.setAttribute(cfg.attribute, dir);
|
|
105
92
|
el.setAttribute("dir", dir);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!hydrating) {
|
|
96
|
+
applyToDocument(untrack(direction));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* After hydrate, sync from localStorage so useDirection() matches DirectionScript. */
|
|
100
|
+
onSettled(() => {
|
|
101
|
+
if (typeof localStorage === "undefined") return;
|
|
102
|
+
try {
|
|
103
|
+
const stored = localStorage.getItem(cfg.storageKey);
|
|
104
|
+
if ((stored === "ltr" || stored === "rtl") && stored !== untrack(direction)) {
|
|
105
|
+
setDirectionSignal(stored);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
/* noop */
|
|
109
|
+
}
|
|
110
|
+
applyToDocument(untrack(direction));
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
/* Cross-tab sync: StorageEvent fires in OTHER tabs only */
|
|
109
|
-
|
|
114
|
+
onSettled(() => {
|
|
115
|
+
if (typeof window === "undefined") return;
|
|
110
116
|
const handler = (e: StorageEvent) => {
|
|
111
117
|
if (e.key !== cfg.storageKey || e.storageArea !== localStorage) return;
|
|
112
118
|
const v = e.newValue;
|
|
@@ -115,22 +121,41 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
115
121
|
}
|
|
116
122
|
};
|
|
117
123
|
window.addEventListener("storage", handler);
|
|
118
|
-
|
|
124
|
+
return () => window.removeEventListener("storage", handler);
|
|
119
125
|
});
|
|
120
126
|
|
|
121
|
-
/*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
/* After Solid hydrates <html> it may drop DirectionScript's dir. Re-apply. */
|
|
128
|
+
let skipFirst = true;
|
|
129
|
+
createEffect(direction, (dir) => {
|
|
130
|
+
if (typeof document === "undefined") return;
|
|
131
|
+
if (skipFirst) {
|
|
132
|
+
skipFirst = false;
|
|
133
|
+
let visual: Direction = dir;
|
|
134
|
+
if (typeof localStorage !== "undefined") {
|
|
126
135
|
try {
|
|
127
|
-
localStorage.
|
|
136
|
+
const stored = localStorage.getItem(cfg.storageKey);
|
|
137
|
+
if (stored === "ltr" || stored === "rtl") visual = stored;
|
|
128
138
|
} catch {
|
|
129
139
|
/* noop */
|
|
130
140
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
}
|
|
142
|
+
applyToDocument(visual);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
applyToDocument(dir);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/* Persist to localStorage on changes (not initial — it was read from there) */
|
|
149
|
+
createEffect(
|
|
150
|
+
direction,
|
|
151
|
+
(dir) => {
|
|
152
|
+
try {
|
|
153
|
+
localStorage.setItem(cfg.storageKey, dir);
|
|
154
|
+
} catch {
|
|
155
|
+
/* noop */
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{ defer: true },
|
|
134
159
|
);
|
|
135
160
|
|
|
136
161
|
const setDirection = (dir: Direction): void => {
|
|
@@ -138,7 +163,7 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
138
163
|
};
|
|
139
164
|
|
|
140
165
|
const toggleDirection = (): void => {
|
|
141
|
-
const current = direction
|
|
166
|
+
const current = untrack(direction);
|
|
142
167
|
setDirection(current === "ltr" ? "rtl" : "ltr");
|
|
143
168
|
};
|
|
144
169
|
|
|
@@ -153,7 +178,7 @@ export function DirectionProvider(props: { children: JSX.Element; config?: Direc
|
|
|
153
178
|
toggleDirection,
|
|
154
179
|
};
|
|
155
180
|
|
|
156
|
-
return <DirectionCtx
|
|
181
|
+
return <DirectionCtx value={value}>{props.children}</DirectionCtx>;
|
|
157
182
|
}
|
|
158
183
|
|
|
159
184
|
export function useDirection(): DirectionContextValue {
|