@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/navigation/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { JSX } from "
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
2
|
import type {
|
|
3
3
|
ClientErrorRenderProps,
|
|
4
4
|
ClientNotFoundRenderProps,
|
|
@@ -29,6 +29,11 @@ export interface LoadedRouteModule {
|
|
|
29
29
|
errorRender?: (props: ClientErrorRenderProps) => JSX.Element;
|
|
30
30
|
notFoundRender?: (props: ClientNotFoundRenderProps) => JSX.Element;
|
|
31
31
|
cache?: {
|
|
32
|
+
cdn?:
|
|
33
|
+
| {
|
|
34
|
+
tags?: string[] | ((ctx: { params: Record<string, string | string[]> }) => string[]);
|
|
35
|
+
}
|
|
36
|
+
| false;
|
|
32
37
|
client?:
|
|
33
38
|
| {
|
|
34
39
|
cacheDeferred?: boolean;
|
|
@@ -38,6 +43,11 @@ export interface LoadedRouteModule {
|
|
|
38
43
|
staleTime?: number;
|
|
39
44
|
}
|
|
40
45
|
| false;
|
|
46
|
+
ssr?:
|
|
47
|
+
| {
|
|
48
|
+
tags?: string[] | ((ctx: { params: Record<string, string | string[]> }) => string[]);
|
|
49
|
+
}
|
|
50
|
+
| false;
|
|
41
51
|
};
|
|
42
52
|
render: (props: RenderProps) => JSX.Element;
|
|
43
53
|
unauthenticatedRender?: (props: ClientUnauthenticatedRenderProps) => JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createEffect,
|
|
2
|
-
import {
|
|
1
|
+
import { createEffect, useContext } from "solid-js";
|
|
2
|
+
import type { JSX } from "@solidjs/web";
|
|
3
|
+
import { isServer } from "@solidjs/web";
|
|
3
4
|
import { RouterContext } from "../outlet/index.tsx";
|
|
4
5
|
|
|
5
6
|
export interface NavigationProgressProps {
|
|
@@ -179,20 +180,20 @@ export function NavigationProgress(props: NavigationProgressProps): JSX.Element
|
|
|
179
180
|
const ctx = useContext(RouterContext);
|
|
180
181
|
|
|
181
182
|
if (!isServer && ctx) {
|
|
182
|
-
createEffect(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
183
|
+
createEffect(
|
|
184
|
+
() => ctx.navigationPhase(),
|
|
185
|
+
(phase) => {
|
|
186
|
+
if (phase === "loading") {
|
|
187
|
+
clearTimers();
|
|
188
|
+
start();
|
|
189
|
+
} else if (phase === "idle" && status !== null) {
|
|
190
|
+
handleComplete();
|
|
191
|
+
} else if (phase === "transitioning" && status !== null) {
|
|
192
|
+
handleComplete();
|
|
193
|
+
}
|
|
194
|
+
return clearTimers;
|
|
195
|
+
},
|
|
196
|
+
);
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
const barColor = (): string => props.color ?? "currentColor";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NotFoundError, RedirectResponse, UnauthenticatedError, UnauthorizedError } from "../errors/index.ts";
|
|
2
2
|
import { warn } from "../logger.ts";
|
|
3
|
+
import { HEADER_DATA, HEADER_FLAG, HEADER_PREFETCH, HEADER_STALE } from "../protocol.ts";
|
|
3
4
|
import type { HeadConfig } from "../route-builder/types.ts";
|
|
4
5
|
import type { DeferredResolver } from "../state-parser/index.ts";
|
|
5
6
|
import { hydrateLoaderData } from "../state-parser/index.ts";
|
|
@@ -8,9 +9,13 @@ const MAX_LINE_BYTES = 5 * 1024 * 1024;
|
|
|
8
9
|
const MAX_MESSAGES = 10_000;
|
|
9
10
|
|
|
10
11
|
export interface NDJSONFetchOptions {
|
|
12
|
+
/** Match ids whose prefetch `l` shell is already committed — skip hydrate, stream `c` into `resolvers`. */
|
|
13
|
+
keepMatchIds?: Iterable<string>;
|
|
11
14
|
matchIds?: string[];
|
|
12
15
|
prefetch?: boolean;
|
|
13
16
|
queryClient?: unknown;
|
|
17
|
+
/** Pre-seeded deferred resolvers (hydrated prefetch shell). */
|
|
18
|
+
resolvers?: Map<string, DeferredResolver>;
|
|
14
19
|
signal?: AbortSignal;
|
|
15
20
|
url: string;
|
|
16
21
|
}
|
|
@@ -18,12 +23,15 @@ export interface NDJSONFetchOptions {
|
|
|
18
23
|
export interface NDJSONFetchResult {
|
|
19
24
|
matches: FetchedMatch[];
|
|
20
25
|
perRouteHeads: PerRouteHead[];
|
|
26
|
+
status?: number;
|
|
21
27
|
success: boolean;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export interface FetchedMatch {
|
|
25
31
|
error?: Error;
|
|
26
32
|
hasDeferredMarkers?: boolean;
|
|
33
|
+
/** Enter `l` for a match whose prefetch shell is already on screen — do not replace loaderData. */
|
|
34
|
+
keepShell?: boolean;
|
|
27
35
|
loaderData: unknown;
|
|
28
36
|
matchId: string;
|
|
29
37
|
preloaderContext?: Record<string, unknown>;
|
|
@@ -94,15 +102,29 @@ function forwardServerLog(msg: NDJSONMessage): void {
|
|
|
94
102
|
}
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
/** Hydrate `t:"q"` entries unless the navigation that started this hop has aborted. */
|
|
106
|
+
export function applyQueryCacheHydration(queryClient: unknown, entries: unknown, signal?: AbortSignal): void {
|
|
107
|
+
if (signal?.aborted) return;
|
|
108
|
+
void import("../query-client")
|
|
109
|
+
.then(({ hydrateQueryCache }) => {
|
|
110
|
+
if (signal?.aborted) return;
|
|
111
|
+
hydrateQueryCache(
|
|
112
|
+
queryClient as Parameters<typeof hydrateQueryCache>[0],
|
|
113
|
+
entries as Parameters<typeof hydrateQueryCache>[1],
|
|
114
|
+
);
|
|
115
|
+
})
|
|
116
|
+
.catch(() => {});
|
|
117
|
+
}
|
|
118
|
+
|
|
97
119
|
export async function fetchNDJSON(options: NDJSONFetchOptions): Promise<NDJSONFetchResult> {
|
|
98
|
-
const headers: Record<string, string> = {
|
|
120
|
+
const headers: Record<string, string> = { [HEADER_DATA]: HEADER_FLAG };
|
|
99
121
|
|
|
100
122
|
if (options.matchIds) {
|
|
101
|
-
headers[
|
|
123
|
+
headers[HEADER_STALE] = options.matchIds.join(",");
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
if (options.prefetch) {
|
|
105
|
-
headers[
|
|
127
|
+
headers[HEADER_PREFETCH] = HEADER_FLAG;
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
const response = await fetch(options.url, {
|
|
@@ -116,12 +138,13 @@ export async function fetchNDJSON(options: NDJSONFetchOptions): Promise<NDJSONFe
|
|
|
116
138
|
* body makes retry() rebuild matches from a cache that never stored the
|
|
117
139
|
* pipeline error, so the error UI cannot recover. */
|
|
118
140
|
if (!response.ok && !isNdjson) {
|
|
119
|
-
return { matches: [], perRouteHeads: [], success: false };
|
|
141
|
+
return { matches: [], perRouteHeads: [], status: response.status, success: false };
|
|
120
142
|
}
|
|
121
143
|
|
|
122
144
|
const matches: FetchedMatch[] = [];
|
|
123
145
|
const perRouteHeads: PerRouteHead[] = [];
|
|
124
|
-
const deferredResolvers = new Map<string, DeferredResolver>();
|
|
146
|
+
const deferredResolvers = options.resolvers ?? new Map<string, DeferredResolver>();
|
|
147
|
+
const keepMatchIds = new Set(options.keepMatchIds ?? []);
|
|
125
148
|
|
|
126
149
|
let loadersReadyResolve: () => void = () => {};
|
|
127
150
|
let loadersReadyResolved = false;
|
|
@@ -168,20 +191,29 @@ export async function fetchNDJSON(options: NDJSONFetchOptions): Promise<NDJSONFe
|
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
case "l": {
|
|
171
|
-
|
|
194
|
+
const matchId = msg.m ?? "";
|
|
195
|
+
if (keepMatchIds.has(matchId)) {
|
|
196
|
+
matches.push({
|
|
197
|
+
hasDeferredMarkers: true,
|
|
198
|
+
keepShell: true,
|
|
199
|
+
loaderData: msg.d,
|
|
200
|
+
matchId,
|
|
201
|
+
preloaderContext: msg.p,
|
|
202
|
+
});
|
|
203
|
+
} else if (options.prefetch) {
|
|
172
204
|
matches.push({
|
|
173
205
|
hasDeferredMarkers: containsDeferredMarkers(msg.d) || undefined,
|
|
174
206
|
loaderData: msg.d,
|
|
175
|
-
matchId
|
|
207
|
+
matchId,
|
|
176
208
|
preloaderContext: msg.p,
|
|
177
209
|
});
|
|
178
210
|
} else {
|
|
179
211
|
const sizeBefore = deferredResolvers.size;
|
|
180
|
-
const hydrated = hydrateLoaderData(
|
|
212
|
+
const hydrated = hydrateLoaderData(matchId, msg.d, deferredResolvers);
|
|
181
213
|
matches.push({
|
|
182
214
|
hasDeferredMarkers: deferredResolvers.size > sizeBefore || undefined,
|
|
183
215
|
loaderData: hydrated,
|
|
184
|
-
matchId
|
|
216
|
+
matchId,
|
|
185
217
|
preloaderContext: msg.p,
|
|
186
218
|
});
|
|
187
219
|
}
|
|
@@ -246,14 +278,7 @@ export async function fetchNDJSON(options: NDJSONFetchOptions): Promise<NDJSONFe
|
|
|
246
278
|
|
|
247
279
|
case "q": {
|
|
248
280
|
if (options.queryClient && Array.isArray(msg.d)) {
|
|
249
|
-
|
|
250
|
-
.then(({ hydrateQueryCache }) => {
|
|
251
|
-
hydrateQueryCache(
|
|
252
|
-
options.queryClient as Parameters<typeof hydrateQueryCache>[0],
|
|
253
|
-
msg.d as Parameters<typeof hydrateQueryCache>[1],
|
|
254
|
-
);
|
|
255
|
-
})
|
|
256
|
-
.catch(() => {});
|
|
281
|
+
applyQueryCacheHydration(options.queryClient, msg.d, options.signal);
|
|
257
282
|
}
|
|
258
283
|
break;
|
|
259
284
|
}
|
|
@@ -6,10 +6,27 @@ import type { HeadConfig } from "../route-builder/types.ts";
|
|
|
6
6
|
import type { ServerLogEntry } from "@lovrozagar/flare/server-context";
|
|
7
7
|
|
|
8
8
|
export interface NDJSONResponseConfig {
|
|
9
|
+
abortController?: AbortController;
|
|
9
10
|
deferContexts: Map<string, DeferContext>;
|
|
10
11
|
matches: PipelineMatch[];
|
|
11
12
|
queryClient?: unknown;
|
|
12
13
|
serverLogs?: ServerLogEntry[];
|
|
14
|
+
status?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function followAbortSignal(controller: AbortController, signal: AbortSignal | undefined): void {
|
|
18
|
+
if (!signal) return;
|
|
19
|
+
if (signal.aborted) {
|
|
20
|
+
controller.abort();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
signal.addEventListener(
|
|
24
|
+
"abort",
|
|
25
|
+
() => {
|
|
26
|
+
if (!controller.signal.aborted) controller.abort();
|
|
27
|
+
},
|
|
28
|
+
{ once: true },
|
|
29
|
+
);
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
const NDJSON_HEADERS = {
|
|
@@ -190,16 +207,44 @@ export function createNDJSONResponse(config: NDJSONResponseConfig): Response {
|
|
|
190
207
|
for (const l of lines) {
|
|
191
208
|
body += `${l}\n`;
|
|
192
209
|
}
|
|
193
|
-
return new Response(body, { headers: NDJSON_HEADERS, status: 200 });
|
|
210
|
+
return new Response(body, { headers: NDJSON_HEADERS, status: config.status ?? 200 });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function createErrorNDJSONResponse(error: Error, status?: number): Response {
|
|
214
|
+
const http =
|
|
215
|
+
status ??
|
|
216
|
+
(error.name === "UnauthenticatedError"
|
|
217
|
+
? 401
|
|
218
|
+
: error.name === "UnauthorizedError"
|
|
219
|
+
? 403
|
|
220
|
+
: error.name === "NotFoundError"
|
|
221
|
+
? 404
|
|
222
|
+
: 500);
|
|
223
|
+
const lines = [formatErrorMessage("", error), formatReadyMessage(), formatDoneMessage()];
|
|
224
|
+
let body = "";
|
|
225
|
+
for (const l of lines) {
|
|
226
|
+
body += `${l}\n`;
|
|
227
|
+
}
|
|
228
|
+
return new Response(body, { headers: NDJSON_HEADERS, status: http });
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
export function createStreamingNDJSONResponse(config: NDJSONResponseConfig): Response {
|
|
197
232
|
const { deferContexts, matches } = config;
|
|
233
|
+
let cancelled = false;
|
|
198
234
|
|
|
199
235
|
const stream = new ReadableStream({
|
|
236
|
+
cancel() {
|
|
237
|
+
cancelled = true;
|
|
238
|
+
if (config.abortController && !config.abortController.signal.aborted) {
|
|
239
|
+
config.abortController.abort();
|
|
240
|
+
}
|
|
241
|
+
},
|
|
200
242
|
async start(controller) {
|
|
201
243
|
const encoder = new TextEncoder();
|
|
202
|
-
const enqueue = (msg: string) =>
|
|
244
|
+
const enqueue = (msg: string) => {
|
|
245
|
+
if (cancelled) return;
|
|
246
|
+
controller.enqueue(encoder.encode(`${msg}\n`));
|
|
247
|
+
};
|
|
203
248
|
|
|
204
249
|
/* 0. Server log messages */
|
|
205
250
|
if (config.serverLogs) {
|
|
@@ -227,14 +272,6 @@ export function createStreamingNDJSONResponse(config: NDJSONResponseConfig): Res
|
|
|
227
272
|
/* 3. Ready */
|
|
228
273
|
enqueue(formatReadyMessage());
|
|
229
274
|
|
|
230
|
-
/* Install QC tracking for deferred callbacks */
|
|
231
|
-
let trackedQC: { drain(): Array<{ data: unknown; key: unknown[]; staleTime?: number }> } | undefined;
|
|
232
|
-
if (config.queryClient) {
|
|
233
|
-
const { createTrackedQueryClient } = await import("../query-client");
|
|
234
|
-
const typed = config.queryClient as Parameters<typeof createTrackedQueryClient>[0];
|
|
235
|
-
trackedQC = createTrackedQueryClient(typed);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
275
|
/* 4. Stream deferred chunks */
|
|
239
276
|
const entries: DeferredEntry[] = [];
|
|
240
277
|
for (const ctx of deferContexts.values()) {
|
|
@@ -242,25 +279,37 @@ export function createStreamingNDJSONResponse(config: NDJSONResponseConfig): Res
|
|
|
242
279
|
entries.push(e);
|
|
243
280
|
}
|
|
244
281
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const qcEntries = trackedQC.drain();
|
|
258
|
-
if (qcEntries.length > 0) {
|
|
259
|
-
enqueue(formatQueryCacheMessage(qcEntries));
|
|
282
|
+
|
|
283
|
+
const streamDeferred = async (trackedQC?: {
|
|
284
|
+
drain(): Array<{ data: unknown; key: unknown[]; staleTime?: number }>;
|
|
285
|
+
}) => {
|
|
286
|
+
await Promise.allSettled(
|
|
287
|
+
entries.map(async (entry) => {
|
|
288
|
+
try {
|
|
289
|
+
const data = await entry.promise;
|
|
290
|
+
enqueue(formatChunkMessage(entry.matchId, entry.key, data));
|
|
291
|
+
} catch (e) {
|
|
292
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
293
|
+
enqueue(formatErrorMessage(entry.matchId, err, entry.key));
|
|
260
294
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
|
|
296
|
+
if (trackedQC) {
|
|
297
|
+
const qcEntries = trackedQC.drain();
|
|
298
|
+
if (qcEntries.length > 0) {
|
|
299
|
+
enqueue(formatQueryCacheMessage(qcEntries));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if (config.queryClient) {
|
|
307
|
+
const { withTrackedQueryClient } = await import("../query-client");
|
|
308
|
+
const typed = config.queryClient as Parameters<typeof withTrackedQueryClient>[0];
|
|
309
|
+
await withTrackedQueryClient(typed, streamDeferred);
|
|
310
|
+
} else {
|
|
311
|
+
await streamDeferred();
|
|
312
|
+
}
|
|
264
313
|
|
|
265
314
|
/* 5. Done */
|
|
266
315
|
enqueue(formatDoneMessage());
|
|
@@ -268,7 +317,7 @@ export function createStreamingNDJSONResponse(config: NDJSONResponseConfig): Res
|
|
|
268
317
|
},
|
|
269
318
|
});
|
|
270
319
|
|
|
271
|
-
return new Response(stream, { headers: NDJSON_HEADERS, status: 200 });
|
|
320
|
+
return new Response(stream, { headers: NDJSON_HEADERS, status: config.status ?? 200 });
|
|
272
321
|
}
|
|
273
322
|
|
|
274
323
|
export function createRedirectNDJSONResponse(redirect: RedirectResponse): Response {
|