@lovrozagar/flare 0.1.1 → 0.2.0

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.
Files changed (85) hide show
  1. package/README.md +44 -19
  2. package/package.json +10 -7
  3. package/src/broadcast/index.ts +1 -1
  4. package/src/broadcast/provider.tsx +2 -2
  5. package/src/caches/index.ts +26 -0
  6. package/src/components/await.tsx +36 -39
  7. package/src/components/dev-error-overlay.tsx +8 -9
  8. package/src/components/direction-script.tsx +2 -2
  9. package/src/components/locale-script.tsx +2 -2
  10. package/src/components/reset-css.tsx +1 -1
  11. package/src/components/ssr-context.tsx +7 -14
  12. package/src/components/theme-script.tsx +2 -2
  13. package/src/components/view-transition-css.tsx +1 -1
  14. package/src/defer/index.ts +22 -0
  15. package/src/direction/index.tsx +61 -36
  16. package/src/errors/index.ts +45 -11
  17. package/src/fonts/component.tsx +2 -1
  18. package/src/form/index.tsx +18 -12
  19. package/src/generators/index.ts +73 -30
  20. package/src/head-client/index.ts +7 -0
  21. package/src/history/index.ts +6 -0
  22. package/src/hydrate/index.tsx +90 -60
  23. package/src/hydration/index.ts +26 -6
  24. package/src/i18n/index.ts +11 -2
  25. package/src/image/index.tsx +7 -4
  26. package/src/intercept-outlet/index.tsx +6 -2
  27. package/src/lazy/index.tsx +50 -50
  28. package/src/link/index.tsx +97 -58
  29. package/src/loader-pipeline/index.ts +93 -35
  30. package/src/locale/cookie.ts +15 -0
  31. package/src/locale/index.tsx +24 -35
  32. package/src/locale.ts +1 -0
  33. package/src/middleware/builtins/api-proxy.ts +58 -4
  34. package/src/middleware/builtins/cdn-proxy.ts +24 -3
  35. package/src/middleware/builtins/i18n.ts +23 -25
  36. package/src/middleware/builtins/keepalive.ts +2 -3
  37. package/src/mount/index.ts +8 -1
  38. package/src/navigation/index.ts +575 -129
  39. package/src/navigation/types.ts +11 -1
  40. package/src/navigation-progress/index.tsx +17 -16
  41. package/src/ndjson-client/index.ts +42 -17
  42. package/src/ndjson-server/index.ts +78 -29
  43. package/src/outlet/index.tsx +192 -97
  44. package/src/outlet/types.ts +9 -1
  45. package/src/plugins/dev-cdn-cache.ts +18 -5
  46. package/src/plugins/dev-dashboard/client.tsx +130 -118
  47. package/src/plugins/dev-prerender.ts +6 -1
  48. package/src/plugins/dev-server.ts +12 -3
  49. package/src/plugins/image-plugin.ts +18 -4
  50. package/src/plugins/index.ts +10 -7
  51. package/src/plugins/prerender-plugin.ts +3 -3
  52. package/src/plugins/server-fn.ts +15 -20
  53. package/src/plugins/solid-jsx.d.ts +3 -3
  54. package/src/plugins/sx-ast/atomic.ts +5 -5
  55. package/src/plugins/sx-ast/index.ts +9 -0
  56. package/src/plugins/sx.d.ts +4 -5
  57. package/src/plugins/virtual.ts +11 -6
  58. package/src/prerender/index.ts +68 -32
  59. package/src/protocol.ts +65 -0
  60. package/src/query-client/index.tsx +73 -14
  61. package/src/rewrite/index.ts +10 -4
  62. package/src/route-builder/create-layout.ts +1 -1
  63. package/src/route-builder/create-root-layout.ts +1 -1
  64. package/src/route-builder/types.ts +5 -1
  65. package/src/router-primitives/types.ts +2 -0
  66. package/src/security/index.ts +41 -5
  67. package/src/server/index.ts +1 -2
  68. package/src/server-fn/get-input.ts +90 -0
  69. package/src/server-fn/http-error.ts +13 -0
  70. package/src/server-fn/index.ts +94 -70
  71. package/src/server-fn-query.ts +9 -17
  72. package/src/server-handler/etag.ts +4 -2
  73. package/src/server-handler/headers.ts +1 -2
  74. package/src/server-handler/index.ts +164 -56
  75. package/src/server-handler/types.ts +2 -0
  76. package/src/service-worker/template.ts +10 -42
  77. package/src/ssr/head-prefix.ts +2 -1
  78. package/src/ssr/hoist-head-markers.ts +25 -0
  79. package/src/ssr/index.tsx +109 -92
  80. package/src/state-parser/index.ts +38 -26
  81. package/src/styles/index.ts +89 -60
  82. package/src/testing/index.ts +8 -3
  83. package/src/theme/index.tsx +35 -32
  84. package/src/tracing/timing.ts +6 -1
  85. 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 (`x-d: 1`). Prefetch, ISR, deferred loaders, and forms reuse that protocol.
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`, `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`.
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-solid
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 (`x-p: 1`) never writes the cookie.
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 (`__flare_qc` / `t:"q"`). `useSuspenseQuery` suspends until the dehydrated entry is ready. Optional `BroadcastProvider` invalidates across tabs.
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 `x-d: 1`. Prefetch also sends `x-p: 1`. Stale match skip uses `x-m`.
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 `vite-plugin-solid` | |
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 + all e2e apps on node
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,9 @@ 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`).
1174
+
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`.
1153
1176
 
1154
1177
  ### Checks
1155
1178
 
@@ -1170,16 +1193,18 @@ Do not weaken `strict` or add `as any` to make typecheck pass.
1170
1193
 
1171
1194
  ### Test matrix
1172
1195
 
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` |
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**) |
1183
1208
 
1184
1209
  ```bash
1185
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.1",
3
+ "version": "0.2.0",
4
4
  "description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
5
5
  "keywords": [
6
6
  "flare",
@@ -16,7 +16,8 @@
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/lovrozagar/flare"
19
+ "url": "https://github.com/lovrozagar/flare",
20
+ "directory": "packages/core"
20
21
  },
21
22
  "files": [
22
23
  "src",
@@ -106,6 +107,8 @@
106
107
  "@capsizecss/metrics": "catalog:",
107
108
  "@formatjs/intl-localematcher": "catalog:",
108
109
  "@solidjs/testing-library": "catalog:",
110
+ "@solidjs/vite-plugin": "catalog:",
111
+ "@solidjs/web": "catalog:",
109
112
  "@tanstack/query-broadcast-client-experimental": "catalog:",
110
113
  "@tanstack/solid-query": "catalog:",
111
114
  "@types/negotiator": "catalog:",
@@ -120,14 +123,15 @@
120
123
  "solid-js": "catalog:",
121
124
  "typescript": "catalog:",
122
125
  "vite": "catalog:",
123
- "vite-plugin-solid": "catalog:",
124
126
  "vitest": "catalog:"
125
127
  },
126
128
  "peerDependencies": {
127
129
  "@babel/parser": ">=8.0.0",
128
130
  "@formatjs/intl-localematcher": ">=0.6.0",
131
+ "@solidjs/vite-plugin": ">=3.0.0-next.0",
132
+ "@solidjs/web": ">=2.0.0-rc.0",
129
133
  "@tanstack/query-broadcast-client-experimental": ">=5.0.0",
130
- "@tanstack/solid-query": ">=5.0.0",
134
+ "@tanstack/solid-query": ">=6.0.0-rc.0",
131
135
  "isbot": ">=5.0.0",
132
136
  "magic-string": ">=0.30.0",
133
137
  "negotiator": ">=1.0.0",
@@ -135,9 +139,8 @@
135
139
  "oxc-parser": ">=0.139.0",
136
140
  "oxc-resolver": ">=11.23.0",
137
141
  "sharp": ">=0.33.0",
138
- "solid-js": ">=1.9.0",
139
- "vite": ">=8.0.0",
140
- "vite-plugin-solid": ">=2.0.0"
142
+ "solid-js": ">=2.0.0-rc.0",
143
+ "vite": ">=8.0.0"
141
144
  },
142
145
  "peerDependenciesMeta": {
143
146
  "@formatjs/intl-localematcher": {
@@ -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>(initialValue);
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 "solid-js";
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.Provider value={props.value ?? noopChannel}>{props.children}</BroadcastCtx.Provider>;
6
+ return <BroadcastCtx value={props.value ?? noopChannel}>{props.children}</BroadcastCtx>;
7
7
  }
@@ -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;
@@ -1,4 +1,5 @@
1
- import { batch, createEffect, createSignal, type JSX } from "solid-js";
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
- const initialResolved = getResolvedValue(props.promise);
51
- const initialError = getResolvedError(props.promise);
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(props.promise);
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
- batch(() => {
74
- setData(() => result);
75
- setStatus("success");
76
- });
76
+ setData(() => result);
77
+ setStatus("success");
77
78
  }
78
79
  },
79
80
  (err: unknown) => {
80
81
  if (currentPromise === p) {
81
- batch(() => {
82
- setError(() => (err instanceof Error ? err : new Error(String(err))));
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
- batch(() => {
94
- setStatus("pending");
95
- setData(undefined);
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
- createEffect(() => {
103
- const newPromise = getPromise(props.promise);
104
- if (newPromise === currentPromise) return;
105
- currentPromise = newPromise;
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
- const resolved = getResolvedValue(props.promise);
108
- const resolvedError = getResolvedError(props.promise);
107
+ const resolved = getResolvedValue(promise);
108
+ const resolvedError = getResolvedError(promise);
109
109
 
110
- if (resolved !== undefined) {
111
- batch(() => {
110
+ if (resolved !== undefined) {
112
111
  setData(() => resolved);
113
112
  setStatus("success");
114
- });
115
- return;
116
- }
117
- if (resolvedError) {
118
- batch(() => {
113
+ return;
114
+ }
115
+ if (resolvedError) {
119
116
  setError(() => resolvedError);
120
117
  setStatus("error");
121
- });
122
- return;
123
- }
118
+ return;
119
+ }
124
120
 
125
- setStatus("pending");
126
- if (newPromise) trackPromise(newPromise);
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, type JSX, onCleanup, onMount, Show } from "solid-js";
2
- import { Portal } from "solid-js/web";
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
- onMount(() => {
38
+ onSettled(() => {
38
39
  if (typeof window === "undefined") return;
39
40
  window.addEventListener("error", handleWindowError);
40
41
  window.addEventListener("unhandledrejection", handleUnhandledRejection);
41
- });
42
-
43
- onCleanup(() => {
44
- if (typeof window === "undefined") return;
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 "solid-js";
2
- import { NoHydration } from "solid-js/web";
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 "solid-js";
2
- import { NoHydration } from "solid-js/web";
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,4 @@
1
- import type { JSX } from "solid-js";
1
+ import type { JSX } from "@solidjs/web";
2
2
  import { useSSRContext } from "./ssr-context.tsx";
3
3
 
4
4
  /* Duplicated from index.ts to avoid circular import (index re-exports this component) */
@@ -1,4 +1,5 @@
1
- import { createContext, type JSX, sharedConfig, useContext } from "solid-js";
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
- export function setSSRContext(value: SSRContextValue): void {
21
- if (sharedConfig.context) {
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
- /* SSR path: read from sharedConfig.context */
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.Provider value={props.value}>{props.children}</SSRCtx.Provider>;
29
+ return <SSRCtx value={props.value}>{props.children}</SSRCtx>;
37
30
  }
@@ -1,5 +1,5 @@
1
- import type { JSX } from "solid-js";
2
- import { NoHydration } from "solid-js/web";
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";
@@ -1,4 +1,4 @@
1
- import type { JSX } from "solid-js";
1
+ import type { JSX } from "@solidjs/web";
2
2
  import { useSSRContext } from "./ssr-context.tsx";
3
3
 
4
4
  /* Duplicated from index.ts to avoid circular import (index re-exports this component) */
@@ -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
+ }