@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.
- package/README.md +44 -19
- package/package.json +10 -7
- 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 +7 -0
- 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 +93 -35
- 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 +164 -56
- 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/src/hydrate/index.tsx
CHANGED
|
@@ -3,8 +3,10 @@ import swConfig from "virtual:flare-sw-config";
|
|
|
3
3
|
|
|
4
4
|
declare const __FLARE_IS_DEV__: boolean | undefined;
|
|
5
5
|
const isDevDefault = typeof __FLARE_IS_DEV__ === "boolean" ? __FLARE_IS_DEV__ : false;
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { flush } from "solid-js";
|
|
7
|
+
import type { JSX } from "@solidjs/web";
|
|
8
|
+
import { createComponent, Hydration, render, hydrate as solidHydrate } from "@solidjs/web";
|
|
9
|
+
import { SSRContextProvider } from "../components/ssr-context.tsx";
|
|
8
10
|
import { createChannel } from "../broadcast/channel.ts";
|
|
9
11
|
import { BroadcastProvider } from "../broadcast/provider.tsx";
|
|
10
12
|
import type { MatchCache } from "../caches/index.ts";
|
|
@@ -17,9 +19,11 @@ import {
|
|
|
17
19
|
extractRootBoundaries,
|
|
18
20
|
hydrateHeadState,
|
|
19
21
|
loadRouteModules,
|
|
22
|
+
applyMatchCacheTags,
|
|
20
23
|
populateMatchCache,
|
|
21
24
|
} from "../hydration/index.ts";
|
|
22
25
|
import { onceIdle } from "../internal/once-idle.ts";
|
|
26
|
+
import { ATTR_HYDRATED } from "../protocol.ts";
|
|
23
27
|
import { setupNavigation } from "../navigation/index.ts";
|
|
24
28
|
import type { LoadedRouteModule } from "../navigation/types.ts";
|
|
25
29
|
import { FlareProvider, Outlet, useRouter } from "../outlet/index.tsx";
|
|
@@ -55,13 +59,10 @@ export interface HydrateOptions {
|
|
|
55
59
|
onInteraction?: () => void;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
function Dummy(props: { children: JSX.Element }): JSX.Element {
|
|
64
|
-
return props.children;
|
|
62
|
+
function readDocumentNonce(): string {
|
|
63
|
+
if (typeof document === "undefined") return "";
|
|
64
|
+
const meta = document.querySelector('meta[name="csp-nonce"]');
|
|
65
|
+
return meta?.getAttribute("content") || meta?.getAttribute("nonce") || "";
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function RootRenderer(props: {
|
|
@@ -71,8 +72,11 @@ function RootRenderer(props: {
|
|
|
71
72
|
renderFn: (p: RenderProps) => JSX.Element;
|
|
72
73
|
}): JSX.Element {
|
|
73
74
|
const router = useRouter();
|
|
75
|
+
/* Getter so <Outlet> does not consume hydration child ids before <html>. */
|
|
74
76
|
return props.renderFn({
|
|
75
|
-
children
|
|
77
|
+
get children() {
|
|
78
|
+
return <Outlet />;
|
|
79
|
+
},
|
|
76
80
|
loaderData: props.data,
|
|
77
81
|
location: props.location,
|
|
78
82
|
preloaderContext: props.preloaderContext,
|
|
@@ -156,7 +160,7 @@ export async function hydrate(router: RouterArg, options?: HydrateOptions): Prom
|
|
|
156
160
|
}
|
|
157
161
|
const { QueryClientProvider } = await import("../query-client");
|
|
158
162
|
QCP = QueryClientProvider as typeof QCP;
|
|
159
|
-
|
|
163
|
+
/* `__flare_queries` is drained after solidHydrate — observers must attach first. */
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
/* Build composed rewrite: basePath (if any) + user rewrite */
|
|
@@ -178,6 +182,7 @@ export async function hydrate(router: RouterArg, options?: HydrateOptions): Prom
|
|
|
178
182
|
const rootLayout = modules.layouts.find((m) => m._type === "root-layout");
|
|
179
183
|
const nonRootLayouts = modules.layouts.filter((m) => m._type !== "root-layout");
|
|
180
184
|
const allModules: LoadedRouteModule[] = [...nonRootLayouts, modules.page];
|
|
185
|
+
applyMatchCacheTags(matchCache, allModules, modules.params, search);
|
|
181
186
|
|
|
182
187
|
/* Build error map from per-match error names (survives serialization) */
|
|
183
188
|
const errorMap = new Map<string, Error>();
|
|
@@ -199,61 +204,71 @@ export async function hydrate(router: RouterArg, options?: HydrateOptions): Prom
|
|
|
199
204
|
const rootMatchState = rootLayout ? state.matches.find((m) => m.virtualPath === rootLayout.virtualPath) : undefined;
|
|
200
205
|
|
|
201
206
|
/*
|
|
202
|
-
* Full-document hydration
|
|
203
|
-
*
|
|
207
|
+
* Full-document hydration. SSR and client wrappers must match —
|
|
208
|
+
* Solid 2 `_hk` ids follow the owner tree.
|
|
204
209
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
210
|
+
* Both: Hydration > QCP? > SSRContextProvider > Theme > Direction >
|
|
211
|
+
* Broadcast > FlareProvider > rootRenderFn({children: Outlet})
|
|
207
212
|
*/
|
|
208
213
|
const channel = createChannel();
|
|
209
214
|
const renderInner = () => (
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
215
|
+
<SSRContextProvider
|
|
216
|
+
value={{
|
|
217
|
+
direction: r.direction,
|
|
218
|
+
flareStateScript: "",
|
|
219
|
+
isServer: false,
|
|
220
|
+
nonce: readDocumentNonce(),
|
|
221
|
+
theme: r.theme,
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<ThemeProvider config={r.theme}>
|
|
225
|
+
<DirectionProvider config={r.direction}>
|
|
226
|
+
<BroadcastProvider value={channel}>
|
|
227
|
+
<FlareProvider
|
|
228
|
+
boundaries={rootBoundaries}
|
|
229
|
+
caseSensitive={r.caseSensitive}
|
|
230
|
+
layouts={r.layouts}
|
|
231
|
+
localeConfig={r.locale}
|
|
232
|
+
matchCache={matchCache}
|
|
233
|
+
matches={initialMatches}
|
|
234
|
+
onContextReady={(ctx) => {
|
|
235
|
+
resolveCtx(ctx);
|
|
236
|
+
}}
|
|
237
|
+
params={state.params}
|
|
238
|
+
prefetchCache={prefetchCache}
|
|
239
|
+
resolvers={state.resolvers}
|
|
240
|
+
routerCacheDefaults={r.cache?.client || undefined}
|
|
241
|
+
routeTree={r.routeTree}
|
|
242
|
+
search={search}
|
|
243
|
+
>
|
|
244
|
+
{rootLayout?.render ? (
|
|
245
|
+
RootRenderer({
|
|
246
|
+
data: rootMatchState?.loaderData,
|
|
247
|
+
location: {
|
|
248
|
+
hash: window.location.hash,
|
|
249
|
+
params: state.params,
|
|
250
|
+
pathname: state.pathname,
|
|
251
|
+
search,
|
|
252
|
+
url: new URL(window.location.href),
|
|
253
|
+
variablePath: "",
|
|
254
|
+
virtualPath: rootLayout.virtualPath,
|
|
255
|
+
},
|
|
256
|
+
preloaderContext: rootMatchState?.preloaderContext,
|
|
257
|
+
renderFn: rootLayout.render,
|
|
258
|
+
})
|
|
259
|
+
) : (
|
|
260
|
+
<Outlet />
|
|
261
|
+
)}
|
|
262
|
+
</FlareProvider>
|
|
263
|
+
</BroadcastProvider>
|
|
264
|
+
</DirectionProvider>
|
|
265
|
+
</ThemeProvider>
|
|
266
|
+
</SSRContextProvider>
|
|
252
267
|
);
|
|
253
268
|
|
|
254
269
|
solidHydrate(
|
|
255
270
|
() => (
|
|
256
|
-
<
|
|
271
|
+
<Hydration>
|
|
257
272
|
{QCP && queryClientInstance
|
|
258
273
|
? createComponent(QCP, {
|
|
259
274
|
get children() {
|
|
@@ -262,12 +277,27 @@ export async function hydrate(router: RouterArg, options?: HydrateOptions): Prom
|
|
|
262
277
|
client: queryClientInstance,
|
|
263
278
|
})
|
|
264
279
|
: renderInner()}
|
|
265
|
-
</
|
|
280
|
+
</Hydration>
|
|
266
281
|
),
|
|
267
282
|
document,
|
|
268
283
|
);
|
|
284
|
+
flush();
|
|
269
285
|
finishHydration();
|
|
270
286
|
|
|
287
|
+
if (queryClientInstance) {
|
|
288
|
+
/*
|
|
289
|
+
* TanStack `useQuery` (no HydrationCoordinator) attaches its cache
|
|
290
|
+
* subscriber on a microtask after the hydration replay. Streamed
|
|
291
|
+
* `__flare_queries` must land after that attach — applying it earlier
|
|
292
|
+
* writes deferred data into a cache with no observers, and the
|
|
293
|
+
* SSR-serialized fallback stays on screen.
|
|
294
|
+
*/
|
|
295
|
+
await Promise.resolve();
|
|
296
|
+
await Promise.resolve();
|
|
297
|
+
installQueryCacheResolver(queryClientInstance);
|
|
298
|
+
flush();
|
|
299
|
+
}
|
|
300
|
+
|
|
271
301
|
replaceHistoryState(window.location.pathname, state.params, window.location.search, {
|
|
272
302
|
hash: window.location.hash,
|
|
273
303
|
historyIndex: 0,
|
|
@@ -352,7 +382,7 @@ export async function hydrate(router: RouterArg, options?: HydrateOptions): Prom
|
|
|
352
382
|
render(() => <DevErrorOverlay />, overlayRoot);
|
|
353
383
|
}
|
|
354
384
|
|
|
355
|
-
document.documentElement.setAttribute(
|
|
385
|
+
document.documentElement.setAttribute(ATTR_HYDRATED, "");
|
|
356
386
|
}
|
|
357
387
|
|
|
358
388
|
/* Service worker registration / unregistration */
|
package/src/hydration/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { batch } from "solid-js";
|
|
2
1
|
import type { GlobalBoundaries } from "../boundaries/index.ts";
|
|
3
|
-
import type
|
|
2
|
+
import { resolveCacheTags, type CachedMatch, type MatchCache } from "../caches/index.ts";
|
|
4
3
|
import { applyPerRouteHeads, initRouteHierarchy } from "../head-client/index.ts";
|
|
5
4
|
import { isRenderFn, retryImport } from "../internal.ts";
|
|
6
5
|
import type { LoadedRouteModule, LoadedRouteModules } from "../navigation/types.ts";
|
|
@@ -80,10 +79,8 @@ export function buildMatches(ctx: FlareProviderContext, modules: LoadedRouteModu
|
|
|
80
79
|
};
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
ctx.setParams(modules.params);
|
|
86
|
-
});
|
|
82
|
+
ctx.setMatches(clientMatches);
|
|
83
|
+
ctx.setParams(modules.params);
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
export function extractRootBoundaries(rootLayout: LoadedRouteModule | undefined): GlobalBoundaries {
|
|
@@ -171,6 +168,29 @@ export function populateMatchCache(
|
|
|
171
168
|
}
|
|
172
169
|
}
|
|
173
170
|
|
|
171
|
+
export function applyMatchCacheTags(
|
|
172
|
+
matchCache: MatchCache,
|
|
173
|
+
modules: LoadedRouteModule[],
|
|
174
|
+
params: Record<string, string | string[]>,
|
|
175
|
+
search: SearchParams,
|
|
176
|
+
): void {
|
|
177
|
+
for (const mod of modules) {
|
|
178
|
+
const deps = mod.effectsConfig?.loaderDeps?.({ search }) ?? [];
|
|
179
|
+
const matchId = computeMatchId({
|
|
180
|
+
loaderDeps: () => deps,
|
|
181
|
+
params,
|
|
182
|
+
routeId: mod.virtualPath,
|
|
183
|
+
search,
|
|
184
|
+
});
|
|
185
|
+
const entry = matchCache.get(matchId);
|
|
186
|
+
if (!entry) continue;
|
|
187
|
+
const tags = resolveCacheTags(mod.cache, params);
|
|
188
|
+
if (!tags) continue;
|
|
189
|
+
entry.tags = tags;
|
|
190
|
+
matchCache.set(entry);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
174
194
|
export function hydrateHeadState(state: Pick<HydrationState, "ph">): void {
|
|
175
195
|
if (state.ph) {
|
|
176
196
|
/* First entry is root layout — its title is the fallback when pages don't define .head() */
|
package/src/i18n/index.ts
CHANGED
|
@@ -209,6 +209,14 @@ export interface Translator<
|
|
|
209
209
|
) => unknown;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
function neutralizeRichValues(values: Record<string, unknown>): Record<string, unknown> {
|
|
213
|
+
const out: Record<string, unknown> = {};
|
|
214
|
+
for (const [key, value] of Object.entries(values)) {
|
|
215
|
+
out[key] = typeof value === "string" ? value.replaceAll("[[", "[\u200B[").replaceAll("]]", "]\u200B]") : value;
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
212
220
|
export function createTranslator<T extends Record<string, Record<string, string>>>(
|
|
213
221
|
loaded: T,
|
|
214
222
|
locale?: string,
|
|
@@ -235,8 +243,9 @@ export function createTranslator<T extends Record<string, Record<string, string>
|
|
|
235
243
|
const template = flat[key];
|
|
236
244
|
if (template === undefined) return key;
|
|
237
245
|
|
|
238
|
-
/*
|
|
239
|
-
|
|
246
|
+
/* ICU first, but neutralize `[[` / `]]` in user values so they cannot
|
|
247
|
+
inject rich-component markers into a trusted template. */
|
|
248
|
+
const formatted = values ? formatMessage(template, neutralizeRichValues(values), locale) : template;
|
|
240
249
|
|
|
241
250
|
/* Then parse rich text components */
|
|
242
251
|
const parts = parseRichText(formatted, components);
|
package/src/image/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMemo, createSignal,
|
|
1
|
+
import { createMemo, createSignal, omit } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
2
3
|
import { warn } from "../logger.ts";
|
|
3
4
|
|
|
4
5
|
/* ── Static image data ── */
|
|
@@ -99,7 +100,7 @@ export interface StaticResponsiveImageProps extends FlareImgProps {
|
|
|
99
100
|
|
|
100
101
|
export type ImageProps = FillImageProps | FixedImageProps | ResponsiveImageProps | StaticResponsiveImageProps;
|
|
101
102
|
|
|
102
|
-
/* Flattened internal type for
|
|
103
|
+
/* Flattened internal type for omit — avoids TS intersection issues with Omit */
|
|
103
104
|
interface ImagePropsInternal {
|
|
104
105
|
alt: string;
|
|
105
106
|
aspectRatio?: number;
|
|
@@ -326,7 +327,9 @@ function mergeStyles(
|
|
|
326
327
|
/* ── Image component ── */
|
|
327
328
|
|
|
328
329
|
export function Image(props: ImageProps): JSX.Element {
|
|
329
|
-
const
|
|
330
|
+
const local = props as unknown as ImagePropsInternal;
|
|
331
|
+
const rest = omit(
|
|
332
|
+
local,
|
|
330
333
|
"alt",
|
|
331
334
|
"aspectRatio",
|
|
332
335
|
"blurDataURL",
|
|
@@ -345,7 +348,7 @@ export function Image(props: ImageProps): JSX.Element {
|
|
|
345
348
|
"style",
|
|
346
349
|
"width",
|
|
347
350
|
"widths",
|
|
348
|
-
|
|
351
|
+
);
|
|
349
352
|
|
|
350
353
|
const [loaded, setLoaded] = createSignal(false);
|
|
351
354
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Show, useContext } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
2
3
|
import { RouterContext } from "../outlet/index.tsx";
|
|
3
4
|
import type { InterceptedState } from "../outlet/types.ts";
|
|
4
5
|
|
|
@@ -11,8 +12,11 @@ export interface InterceptOutletProps {
|
|
|
11
12
|
/**
|
|
12
13
|
* Renders intercepted route content. SSR-safe — returns null when outside
|
|
13
14
|
* FlareProvider (root layout renders in NoHydration during SSR).
|
|
15
|
+
*
|
|
16
|
+
* Unkeyed on purpose: the function child receives an accessor. Reading it in
|
|
17
|
+
* JSX re-invokes `children` when intercept identity changes (overlay A → B).
|
|
14
18
|
*/
|
|
15
19
|
export function InterceptOutlet(props: InterceptOutletProps): JSX.Element {
|
|
16
20
|
const ctx = useContext(RouterContext);
|
|
17
|
-
return (<Show when={ctx?.intercepted()}>{(state) => props.children(state())}</Show>) as JSX.Element;
|
|
21
|
+
return (<Show when={ctx?.intercepted()}>{(state) => <>{props.children(state())}</>}</Show>) as JSX.Element;
|
|
18
22
|
}
|
package/src/lazy/index.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
2
|
-
import {
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import { Dynamic, isServer } from "@solidjs/web";
|
|
3
|
+
import { type Component, createSignal, onSettled, Show, sharedConfig, untrack } from "solid-js";
|
|
3
4
|
import { retryImport } from "../internal.ts";
|
|
4
5
|
import { warn } from "../logger.ts";
|
|
6
|
+
import { GLOBAL_LAZY_LOADED, GLOBAL_LAZY_PENDING } from "../protocol.ts";
|
|
5
7
|
|
|
6
8
|
export interface LazyOptions<P extends Record<string, unknown>> {
|
|
7
9
|
loader: () => Promise<{ default: Component<P> }>;
|
|
@@ -14,8 +16,8 @@ export interface ClientLazyOptions<P extends Record<string, unknown>> {
|
|
|
14
16
|
pending?: Component<P>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
const PENDING_KEY =
|
|
18
|
-
const LOADED_KEY =
|
|
19
|
+
const PENDING_KEY = GLOBAL_LAZY_PENDING;
|
|
20
|
+
const LOADED_KEY = GLOBAL_LAZY_LOADED;
|
|
19
21
|
|
|
20
22
|
function getGlobalPending(): Set<Promise<void>> {
|
|
21
23
|
const g = globalThis as Record<string, unknown>;
|
|
@@ -29,13 +31,9 @@ function getGlobalLoaded(): Set<Promise<void>> {
|
|
|
29
31
|
return g[LOADED_KEY] as Set<Promise<void>>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/**
|
|
33
|
-
* Throws during component init — ErrorBoundary catches it.
|
|
34
|
-
* Must be rendered via Show/conditional so the throw happens during
|
|
35
|
-
* the component's initial render phase (not in an effect).
|
|
36
|
-
*/
|
|
34
|
+
/** Throw during render so `<Errored>` can catch. Read is untracked — error is a snapshot. */
|
|
37
35
|
function ThrowError(props: { error: Error }): JSX.Element {
|
|
38
|
-
throw props.error;
|
|
36
|
+
throw untrack(() => props.error);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
export function lazy<P extends Record<string, unknown>>(options: LazyOptions<P>): Component<P> {
|
|
@@ -58,44 +56,42 @@ export function lazy<P extends Record<string, unknown>>(options: LazyOptions<P>)
|
|
|
58
56
|
getGlobalPending().add(loadPromise);
|
|
59
57
|
|
|
60
58
|
return ((props: P) => {
|
|
61
|
-
const isSSR = !!sharedConfig.
|
|
59
|
+
const isSSR = isServer || !!sharedConfig.hydrating;
|
|
62
60
|
|
|
63
61
|
/* Pre-known error at render time → throw from component body */
|
|
64
62
|
if (loadError) throw loadError;
|
|
65
63
|
|
|
66
|
-
|
|
64
|
+
/* Store `{ C }` — Solid 2 treats a function initial value as a derived signal. */
|
|
65
|
+
const [component, setComponent] = createSignal<{ C: Component<P> } | undefined>(
|
|
66
|
+
isSSR || !loaded ? undefined : { C: loaded },
|
|
67
|
+
);
|
|
67
68
|
const [error, setError] = createSignal<Error | undefined>();
|
|
68
69
|
|
|
69
70
|
if (isSSR || !loaded) {
|
|
70
|
-
|
|
71
|
+
onSettled(() => {
|
|
71
72
|
if (loadError) {
|
|
72
73
|
setError(loadError);
|
|
73
74
|
} else if (loaded) {
|
|
74
|
-
setComponent(
|
|
75
|
+
setComponent({ C: loaded });
|
|
75
76
|
} else {
|
|
76
77
|
loadPromise?.then(() => {
|
|
77
78
|
if (loadError) setError(loadError);
|
|
78
|
-
else
|
|
79
|
+
else if (loaded) setComponent({ C: loaded });
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
const PendingFallback = pending
|
|
85
|
-
? (() => {
|
|
86
|
-
const P = pending;
|
|
87
|
-
return (<P {...props} />) as JSX.Element;
|
|
88
|
-
})()
|
|
89
|
-
: null;
|
|
90
|
-
|
|
91
85
|
return (
|
|
92
|
-
<Show
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
<Show
|
|
87
|
+
fallback={
|
|
88
|
+
<Show fallback={pending ? <Dynamic component={pending} {...props} /> : null} when={component()}>
|
|
89
|
+
{(entry) => <Dynamic component={entry().C} {...props} />}
|
|
90
|
+
</Show>
|
|
91
|
+
}
|
|
92
|
+
when={error()}
|
|
93
|
+
>
|
|
94
|
+
{(err) => <ThrowError error={err() as Error} />}
|
|
99
95
|
</Show>
|
|
100
96
|
) as JSX.Element;
|
|
101
97
|
}) as Component<P>;
|
|
@@ -109,7 +105,7 @@ export function clientLazy<P extends Record<string, unknown>>(
|
|
|
109
105
|
let loadError: Error | undefined;
|
|
110
106
|
let loadPromise: Promise<void> | undefined;
|
|
111
107
|
|
|
112
|
-
if (eager) {
|
|
108
|
+
if (eager && !isServer) {
|
|
113
109
|
loadPromise = retryImport(loader)
|
|
114
110
|
.then((mod) => {
|
|
115
111
|
loaded = mod.default;
|
|
@@ -120,12 +116,15 @@ export function clientLazy<P extends Record<string, unknown>>(
|
|
|
120
116
|
}
|
|
121
117
|
|
|
122
118
|
return ((props: P & { pending?: Component<P> }) => {
|
|
123
|
-
const isSSR = !!sharedConfig.
|
|
119
|
+
const isSSR = isServer || !!sharedConfig.hydrating;
|
|
124
120
|
|
|
125
121
|
/* Pre-known error at render time → throw from component body */
|
|
126
122
|
if (loadError) throw loadError;
|
|
127
123
|
|
|
128
|
-
|
|
124
|
+
/* Store `{ C }` — Solid 2 treats a function initial value as a derived signal. */
|
|
125
|
+
const [component, setComponent] = createSignal<{ C: Component<P> } | undefined>(
|
|
126
|
+
isSSR || !loaded ? undefined : { C: loaded },
|
|
127
|
+
);
|
|
129
128
|
const [error, setError] = createSignal<Error | undefined>();
|
|
130
129
|
|
|
131
130
|
const startLoading = () => {
|
|
@@ -141,7 +140,7 @@ export function clientLazy<P extends Record<string, unknown>>(
|
|
|
141
140
|
loadPromise
|
|
142
141
|
.then(() => {
|
|
143
142
|
if (loadError) setError(loadError);
|
|
144
|
-
else
|
|
143
|
+
else if (loaded) setComponent({ C: loaded });
|
|
145
144
|
})
|
|
146
145
|
.catch((e: unknown) => {
|
|
147
146
|
warn("lazy", "chunk retry failed", e);
|
|
@@ -156,9 +155,9 @@ export function clientLazy<P extends Record<string, unknown>>(
|
|
|
156
155
|
/* Already loaded — render immediately */
|
|
157
156
|
} else {
|
|
158
157
|
/* SSR hydration or not-yet-loaded: kick off after mount */
|
|
159
|
-
|
|
158
|
+
onSettled(() => {
|
|
160
159
|
if (loaded) {
|
|
161
|
-
setComponent(
|
|
160
|
+
setComponent({ C: loaded });
|
|
162
161
|
} else if (loadError) {
|
|
163
162
|
setError(loadError);
|
|
164
163
|
} else {
|
|
@@ -167,22 +166,23 @@ export function clientLazy<P extends Record<string, unknown>>(
|
|
|
167
166
|
});
|
|
168
167
|
}
|
|
169
168
|
|
|
170
|
-
const PendingComp = props.pending ?? factoryPending;
|
|
171
|
-
const PendingFallback = PendingComp
|
|
172
|
-
? (() => {
|
|
173
|
-
const P = PendingComp;
|
|
174
|
-
return (<P {...(props as P)} />) as JSX.Element;
|
|
175
|
-
})()
|
|
176
|
-
: null;
|
|
177
|
-
|
|
178
169
|
return (
|
|
179
|
-
<Show
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
170
|
+
<Show
|
|
171
|
+
fallback={
|
|
172
|
+
<Show
|
|
173
|
+
fallback={
|
|
174
|
+
<Show when={props.pending ?? factoryPending}>
|
|
175
|
+
{(Comp) => <Dynamic component={Comp()} {...(props as P)} />}
|
|
176
|
+
</Show>
|
|
177
|
+
}
|
|
178
|
+
when={component()}
|
|
179
|
+
>
|
|
180
|
+
{(entry) => <Dynamic component={entry().C} {...(props as P)} />}
|
|
181
|
+
</Show>
|
|
182
|
+
}
|
|
183
|
+
when={error()}
|
|
184
|
+
>
|
|
185
|
+
{(err) => <ThrowError error={err() as Error} />}
|
|
186
186
|
</Show>
|
|
187
187
|
) as JSX.Element;
|
|
188
188
|
}) as Component<P & { pending?: Component<P> }>;
|