@chr33s/solarflare 0.0.2

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 (47) hide show
  1. package/package.json +52 -0
  2. package/readme.md +183 -0
  3. package/src/ast.ts +316 -0
  4. package/src/build.bundle-client.ts +404 -0
  5. package/src/build.bundle-server.ts +131 -0
  6. package/src/build.bundle.ts +48 -0
  7. package/src/build.emit-manifests.ts +25 -0
  8. package/src/build.hmr-entry.ts +88 -0
  9. package/src/build.scan.ts +182 -0
  10. package/src/build.ts +227 -0
  11. package/src/build.validate.ts +63 -0
  12. package/src/client.hmr.ts +78 -0
  13. package/src/client.styles.ts +68 -0
  14. package/src/client.ts +190 -0
  15. package/src/codemod.ts +688 -0
  16. package/src/console-forward.ts +254 -0
  17. package/src/critical-css.ts +103 -0
  18. package/src/devtools-json.ts +52 -0
  19. package/src/diff-dom-streaming.ts +406 -0
  20. package/src/early-flush.ts +125 -0
  21. package/src/early-hints.ts +83 -0
  22. package/src/fetch.ts +44 -0
  23. package/src/fs.ts +11 -0
  24. package/src/head.ts +876 -0
  25. package/src/hmr.ts +647 -0
  26. package/src/hydration.ts +238 -0
  27. package/src/manifest.runtime.ts +25 -0
  28. package/src/manifest.ts +23 -0
  29. package/src/paths.ts +96 -0
  30. package/src/render-priority.ts +69 -0
  31. package/src/route-cache.ts +163 -0
  32. package/src/router-deferred.ts +85 -0
  33. package/src/router-stream.ts +65 -0
  34. package/src/router.ts +535 -0
  35. package/src/runtime.ts +32 -0
  36. package/src/serialize.ts +38 -0
  37. package/src/server.hmr.ts +67 -0
  38. package/src/server.styles.ts +42 -0
  39. package/src/server.ts +480 -0
  40. package/src/solarflare.d.ts +101 -0
  41. package/src/speculation-rules.ts +171 -0
  42. package/src/store.ts +78 -0
  43. package/src/stream-assets.ts +135 -0
  44. package/src/stylesheets.ts +222 -0
  45. package/src/worker.config.ts +243 -0
  46. package/src/worker.ts +542 -0
  47. package/tsconfig.json +21 -0
package/src/worker.ts ADDED
@@ -0,0 +1,542 @@
1
+ import { type FunctionComponent } from "preact";
2
+ import * as server from "./server";
3
+ import type { Route, SolarflareStream } from "./server";
4
+ import { isConsoleRequest, processConsoleLogs, type LogLevel } from "./console-forward.ts";
5
+ import { isDevToolsRequest, handleDevToolsRequest } from "./devtools-json.ts";
6
+ import { isHmrRequest, handleHmrRequest } from "./server.hmr.ts";
7
+ import {
8
+ generateStaticShell,
9
+ createEarlyFlushStream,
10
+ generateResourceHints,
11
+ type StreamingShell,
12
+ } from "./early-flush.ts";
13
+ import { extractCriticalCss, generateAsyncCssLoader } from "./critical-css.ts";
14
+ import { collectEarlyHints, generateEarlyHintsHeader } from "./early-hints.ts";
15
+ import { ResponseCache, withCache } from "./route-cache.ts";
16
+ import { parseMetaConfig } from "./worker.config.ts";
17
+ import { getHeadContext, type HeadTag } from "./head.ts";
18
+ import { typedModules, getScriptPath, getStylesheets, getDevScripts } from "./manifest.runtime.ts";
19
+ import { findPairedModulePath } from "./paths.ts";
20
+ import { encode } from "turbo-stream";
21
+
22
+ const responseCache = new ResponseCache(100);
23
+
24
+ const staticShellCache = new Map<string, StreamingShell>();
25
+
26
+ const PATCH_ENDPOINT = "/_sf/patch";
27
+
28
+ /** Gets or creates a static shell. */
29
+ function getStaticShell(lang: string) {
30
+ let shell = staticShellCache.get(lang);
31
+ if (!shell) {
32
+ shell = generateStaticShell({ lang });
33
+ staticShellCache.set(lang, shell);
34
+ }
35
+ return shell;
36
+ }
37
+
38
+ /** Worker optimization options. */
39
+ export interface WorkerOptimizations {
40
+ earlyFlush?: boolean;
41
+ criticalCss?: boolean;
42
+ readCss?: (path: string) => Promise<string>;
43
+ }
44
+
45
+ /** Server data loader function type. */
46
+ type ServerLoader = (
47
+ request: Request,
48
+ params: Record<string, string>,
49
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
50
+
51
+ const router = server.createRouter(typedModules);
52
+
53
+ /** Worker environment. */
54
+ interface WorkerEnv {
55
+ WRANGLER_LOG?: LogLevel;
56
+ SF_OPTIMIZATIONS?: WorkerOptimizations;
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ interface SsrContext {
61
+ url: URL;
62
+ route: Route;
63
+ params: Record<string, string>;
64
+ content: ReturnType<typeof server.renderComponent>;
65
+ shellData: Record<string, unknown>;
66
+ deferredData: Record<string, Promise<unknown>> | null;
67
+ responseHeaders?: Record<string, string>;
68
+ responseStatus?: number;
69
+ responseStatusText?: string;
70
+ scriptPath?: string;
71
+ stylesheets: string[];
72
+ devScripts?: string[];
73
+ metaConfig: ReturnType<typeof parseMetaConfig>;
74
+ }
75
+
76
+ interface RenderPlan {
77
+ ssrStream: SolarflareStream;
78
+ finalHeaders: Record<string, string>;
79
+ status: number;
80
+ statusText?: string;
81
+ metaConfig: ReturnType<typeof parseMetaConfig>;
82
+ stylesheets: string[];
83
+ resourceHints: string;
84
+ useEarlyFlush: boolean;
85
+ useCriticalCss: boolean;
86
+ pathname: string;
87
+ }
88
+
89
+ type MatchAndLoadResult =
90
+ | { kind: "not-found" }
91
+ | { kind: "api"; response: Response }
92
+ | { kind: "ssr"; context: SsrContext };
93
+
94
+ function getDefaultHeaders() {
95
+ return {
96
+ "Content-Type": "text/html; charset=utf-8",
97
+ "Content-Encoding": "identity",
98
+ "Content-Security-Policy": "frame-ancestors 'self'",
99
+ "Referrer-Policy": "strict-origin-when-cross-origin",
100
+ "Transfer-Encoding": "chunked",
101
+ "X-Content-Type-Options": "nosniff",
102
+ };
103
+ }
104
+
105
+ async function handleDevEndpoints(request: Request, env?: WorkerEnv) {
106
+ if (isHmrRequest(request)) {
107
+ return handleHmrRequest();
108
+ }
109
+
110
+ if (isConsoleRequest(request)) {
111
+ const logLevel = env?.WRANGLER_LOG ?? "log";
112
+ return processConsoleLogs(request, logLevel);
113
+ }
114
+
115
+ if (isDevToolsRequest(request)) {
116
+ return handleDevToolsRequest();
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ async function renderErrorResponse(
123
+ error: Error,
124
+ url: URL,
125
+ status: number,
126
+ headers: Record<string, string>,
127
+ ) {
128
+ const errorContent = await server.renderErrorPage(error, url, typedModules, status);
129
+ const stylesheets = getStylesheets("/");
130
+ const devScripts = getDevScripts();
131
+
132
+ const stream = await server.renderToStream(errorContent, {
133
+ pathname: url.pathname,
134
+ styles: stylesheets,
135
+ devScripts,
136
+ });
137
+
138
+ return new Response(stream, {
139
+ headers,
140
+ status,
141
+ });
142
+ }
143
+
144
+ /** Patch stream payload: meta plus an async iterable of HTML chunks. */
145
+ interface PatchPayload {
146
+ meta: {
147
+ outlet: string;
148
+ head: HeadTag[];
149
+ htmlAttrs: Record<string, string>;
150
+ bodyAttrs: Record<string, string>;
151
+ };
152
+ html: AsyncIterable<string>;
153
+ }
154
+
155
+ /** @yields HTML string chunks decoded from the byte stream. */
156
+ async function* htmlChunkGenerator(htmlStream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
157
+ const decoder = new TextDecoder();
158
+ const reader = htmlStream.getReader();
159
+
160
+ try {
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) break;
164
+ const chunk = decoder.decode(value, { stream: true });
165
+ if (chunk) yield chunk;
166
+ }
167
+ const tail = decoder.decode();
168
+ if (tail) yield tail;
169
+ } finally {
170
+ reader.releaseLock();
171
+ }
172
+ }
173
+
174
+ /** Creates a turbo-stream encoded patch response. */
175
+ function createPatchStream(
176
+ htmlStream: ReadableStream<Uint8Array>,
177
+ outlet: string,
178
+ ): ReadableStream<Uint8Array> {
179
+ const headCtx = getHeadContext();
180
+
181
+ const payload: PatchPayload = {
182
+ meta: {
183
+ outlet,
184
+ head: headCtx.resolveTags(),
185
+ htmlAttrs: headCtx.htmlAttrs,
186
+ bodyAttrs: headCtx.bodyAttrs,
187
+ },
188
+ html: htmlChunkGenerator(htmlStream),
189
+ };
190
+
191
+ // encode() returns ReadableStream<string>, convert to Uint8Array
192
+ const turboStream = encode(payload);
193
+ return turboStream.pipeThrough(new TextEncoderStream());
194
+ }
195
+
196
+ function getPatchHeaders(
197
+ baseHeaders: Record<string, string>,
198
+ responseHeaders?: Record<string, string>,
199
+ ) {
200
+ return {
201
+ ...baseHeaders,
202
+ ...responseHeaders,
203
+ "Cache-Control": "private, no-store",
204
+ "Content-Encoding": "identity",
205
+ "Content-Type": "application/x-turbo-stream; charset=utf-8",
206
+ "X-Content-Type-Options": "nosniff",
207
+ };
208
+ }
209
+
210
+ async function renderPatchResponse(
211
+ context: SsrContext,
212
+ headers: Record<string, string>,
213
+ outlet: string,
214
+ ) {
215
+ const { url, route, params, content, shellData, deferredData } = context;
216
+ const { scriptPath, stylesheets, devScripts } = context;
217
+
218
+ const ssrStream = await server.renderToStream(content, {
219
+ params,
220
+ serverData: shellData,
221
+ pathname: url.pathname,
222
+ script: scriptPath,
223
+ styles: stylesheets,
224
+ devScripts,
225
+ deferred: deferredData ? { tag: route.tag, promises: deferredData } : undefined,
226
+ _headers: context.responseHeaders,
227
+ _status: context.responseStatus,
228
+ _statusText: context.responseStatusText,
229
+ });
230
+
231
+ const patchStream = createPatchStream(ssrStream, outlet);
232
+ const finalHeaders = getPatchHeaders(headers, ssrStream.headers);
233
+
234
+ return new Response(patchStream, {
235
+ headers: finalHeaders,
236
+ status: ssrStream.status ?? 200,
237
+ statusText: ssrStream.statusText,
238
+ });
239
+ }
240
+
241
+ async function matchAndLoad(request: Request, url: URL): Promise<MatchAndLoadResult> {
242
+ const match = server.matchRoute(router, url);
243
+
244
+ if (!match) {
245
+ return { kind: "not-found" };
246
+ }
247
+
248
+ const { route, params } = match;
249
+
250
+ if (route.type === "server") {
251
+ const pairedClientPath = findPairedModulePath(route.path, typedModules);
252
+ if (!pairedClientPath) {
253
+ const mod = await route.loader();
254
+ const handler = mod.default as (request: Request) => Response | Promise<Response>;
255
+ return { kind: "api", response: await handler(request) };
256
+ }
257
+ }
258
+
259
+ let serverPath: string | null = null;
260
+ let clientPath: string;
261
+
262
+ if (route.type === "server") {
263
+ serverPath = route.path;
264
+ clientPath = route.path.replace(".server.", ".client.");
265
+ } else {
266
+ clientPath = route.path;
267
+ serverPath = findPairedModulePath(route.path, typedModules);
268
+ }
269
+
270
+ let shellData: Record<string, unknown> = {};
271
+ let deferredData: Record<string, Promise<unknown>> | null = null;
272
+ let responseHeaders: Record<string, string> | undefined;
273
+ let responseStatus: number | undefined;
274
+ let responseStatusText: string | undefined;
275
+
276
+ if (serverPath && serverPath in typedModules.server) {
277
+ const serverMod = await typedModules.server[serverPath]();
278
+ const loader = serverMod.default as ServerLoader;
279
+ const result = (await loader(request, params)) as Record<string, unknown> & {
280
+ _headers?: Record<string, string>;
281
+ _status?: number;
282
+ _statusText?: string;
283
+ };
284
+
285
+ responseHeaders = result._headers;
286
+ responseStatus = result._status;
287
+ responseStatusText = result._statusText;
288
+
289
+ const immediateData: Record<string, unknown> = {};
290
+ const deferredPromises: Record<string, Promise<unknown>> = {};
291
+
292
+ const dataEntries = Object.entries(result).filter(([key]) => !key.startsWith("_"));
293
+
294
+ for (const [key, value] of dataEntries) {
295
+ if (value instanceof Promise) {
296
+ deferredPromises[key] = value;
297
+ } else {
298
+ immediateData[key] = value;
299
+ }
300
+ }
301
+
302
+ shellData = immediateData;
303
+ deferredData = Object.keys(deferredPromises).length > 0 ? deferredPromises : null;
304
+ }
305
+
306
+ const props: Record<string, unknown> = { ...params, ...shellData };
307
+
308
+ const clientMod = await typedModules.client[clientPath]();
309
+ const Component = clientMod.default as FunctionComponent<any>;
310
+
311
+ let content = server.renderComponent(Component, route.tag, props);
312
+
313
+ const layouts = server.findLayouts(route.path, typedModules);
314
+ if (layouts.length > 0) {
315
+ content = await server.wrapWithLayouts(content, layouts);
316
+ }
317
+
318
+ const scriptPath = getScriptPath(route.tag);
319
+ const stylesheets = getStylesheets(route.parsedPattern.pathname);
320
+ const devScripts = getDevScripts();
321
+
322
+ const headCtx = getHeadContext();
323
+ const headHtml = headCtx.renderToString();
324
+ const metaConfig = parseMetaConfig(headHtml);
325
+
326
+ return {
327
+ kind: "ssr",
328
+ context: {
329
+ url,
330
+ route,
331
+ params,
332
+ content,
333
+ shellData,
334
+ deferredData,
335
+ responseHeaders,
336
+ responseStatus,
337
+ responseStatusText,
338
+ scriptPath,
339
+ stylesheets,
340
+ devScripts,
341
+ metaConfig,
342
+ },
343
+ };
344
+ }
345
+
346
+ async function renderStream(
347
+ context: SsrContext,
348
+ headers: Record<string, string>,
349
+ envOptimizations: WorkerOptimizations,
350
+ ) {
351
+ const { url, route, params, content, shellData, deferredData, metaConfig } = context;
352
+ const { scriptPath, stylesheets, devScripts } = context;
353
+
354
+ const earlyHints = collectEarlyHints({
355
+ scriptPath,
356
+ stylesheets,
357
+ preconnectOrigins: metaConfig.preconnectOrigins,
358
+ });
359
+
360
+ const resourceHints = generateResourceHints({
361
+ scripts: scriptPath ? [scriptPath] : [],
362
+ stylesheets,
363
+ });
364
+
365
+ const useEarlyFlush = envOptimizations.earlyFlush ?? metaConfig.earlyFlush;
366
+ const useCriticalCss = envOptimizations.criticalCss ?? metaConfig.criticalCss;
367
+
368
+ const ssrStream = await server.renderToStream(content, {
369
+ params,
370
+ serverData: shellData,
371
+ pathname: url.pathname,
372
+ script: scriptPath,
373
+ styles: useEarlyFlush ? [] : stylesheets,
374
+ devScripts,
375
+ deferred: deferredData ? { tag: route.tag, promises: deferredData } : undefined,
376
+ _headers: context.responseHeaders,
377
+ _status: context.responseStatus,
378
+ _statusText: context.responseStatusText,
379
+ });
380
+
381
+ const finalHeaders: Record<string, string> = { ...headers };
382
+ if (ssrStream.headers) {
383
+ for (const [key, value] of Object.entries(ssrStream.headers)) {
384
+ finalHeaders[key] = value;
385
+ }
386
+ }
387
+
388
+ if (earlyHints.length > 0) {
389
+ finalHeaders["Link"] = generateEarlyHintsHeader(earlyHints);
390
+ }
391
+
392
+ return {
393
+ ssrStream,
394
+ finalHeaders,
395
+ status: ssrStream.status ?? 200,
396
+ statusText: ssrStream.statusText,
397
+ metaConfig,
398
+ stylesheets,
399
+ resourceHints,
400
+ useEarlyFlush,
401
+ useCriticalCss,
402
+ pathname: route.parsedPattern.pathname,
403
+ };
404
+ }
405
+
406
+ async function applyPerfFeatures(plan: RenderPlan, envOptimizations: WorkerOptimizations) {
407
+ const {
408
+ ssrStream,
409
+ finalHeaders,
410
+ status,
411
+ statusText,
412
+ stylesheets,
413
+ resourceHints,
414
+ useEarlyFlush,
415
+ useCriticalCss,
416
+ pathname,
417
+ metaConfig,
418
+ } = plan;
419
+
420
+ if (useEarlyFlush) {
421
+ const staticShell = getStaticShell(metaConfig.lang);
422
+
423
+ let criticalCss = "";
424
+ if (useCriticalCss && envOptimizations.readCss) {
425
+ criticalCss = await extractCriticalCss(pathname, stylesheets, {
426
+ readCss: envOptimizations.readCss,
427
+ cache: true,
428
+ });
429
+ }
430
+
431
+ const optimizedStream = createEarlyFlushStream(staticShell, {
432
+ criticalCss,
433
+ preloadHints: resourceHints,
434
+ contentStream: ssrStream,
435
+ headTags: "",
436
+ bodyTags: generateAsyncCssLoader(stylesheets),
437
+ });
438
+
439
+ return new Response(optimizedStream, {
440
+ headers: finalHeaders,
441
+ status,
442
+ statusText,
443
+ });
444
+ }
445
+
446
+ return new Response(ssrStream, {
447
+ headers: finalHeaders,
448
+ status,
449
+ statusText,
450
+ });
451
+ }
452
+
453
+ async function handlePatchRequest(request: Request) {
454
+ const url = new URL(request.url);
455
+ if (!(request.method === "POST" && url.pathname === PATCH_ENDPOINT)) return;
456
+
457
+ const headers = getDefaultHeaders();
458
+ try {
459
+ const body = (await request.json()) as { url?: string; outlet?: string };
460
+ if (!body?.url) {
461
+ return new Response("Missing url", { status: 400, headers });
462
+ }
463
+
464
+ const targetUrl = new URL(body.url, url.origin);
465
+ if (targetUrl.origin !== url.origin) {
466
+ return new Response("Invalid url", { status: 400, headers });
467
+ }
468
+
469
+ const targetRequest = new Request(targetUrl, {
470
+ method: "GET",
471
+ headers: request.headers,
472
+ });
473
+
474
+ const result = await matchAndLoad(targetRequest, targetUrl);
475
+
476
+ if (result.kind === "not-found") {
477
+ return new Response("Not Found", { status: 404, headers });
478
+ }
479
+
480
+ if (result.kind === "api") {
481
+ return new Response("Invalid patch target", { status: 400, headers });
482
+ }
483
+
484
+ const outlet = body.outlet ?? "#app";
485
+ return renderPatchResponse(result.context, headers, outlet);
486
+ } catch (error) {
487
+ const serverError = error instanceof Error ? error : new Error(String(error));
488
+ console.error("[solarflare] Patch error:", serverError);
489
+ return new Response("Patch error", { status: 500, headers });
490
+ }
491
+ }
492
+
493
+ /** Cloudflare Worker fetch handler with auto-discovered routes and streaming SSR. */
494
+ async function worker(request: Request, env?: WorkerEnv) {
495
+ const url = new URL(request.url);
496
+ const devResponse = await handleDevEndpoints(request, env);
497
+ if (devResponse) return devResponse;
498
+
499
+ const patchResponse = await handlePatchRequest(request);
500
+ if (patchResponse) return patchResponse;
501
+
502
+ const headers = getDefaultHeaders();
503
+
504
+ try {
505
+ const result = await matchAndLoad(request, url);
506
+
507
+ if (result.kind === "not-found") {
508
+ const notFoundError = new Error(`Page not found: ${url.pathname}`);
509
+ return renderErrorResponse(notFoundError, url, 404, headers);
510
+ }
511
+
512
+ if (result.kind === "api") {
513
+ return result.response;
514
+ }
515
+
516
+ const { context } = result;
517
+ const envOptimizations = env?.SF_OPTIMIZATIONS ?? {};
518
+
519
+ const render = async () => {
520
+ const plan = await renderStream(context, headers, envOptimizations);
521
+ return applyPerfFeatures(plan, envOptimizations);
522
+ };
523
+
524
+ if (context.metaConfig.cacheConfig) {
525
+ return withCache(
526
+ request,
527
+ context.params,
528
+ context.metaConfig.cacheConfig,
529
+ render,
530
+ responseCache,
531
+ );
532
+ }
533
+
534
+ return render();
535
+ } catch (error) {
536
+ const serverError = error instanceof Error ? error : new Error(String(error));
537
+ console.error("[solarflare] Server error:", serverError);
538
+ return renderErrorResponse(serverError, url, 500, headers);
539
+ }
540
+ }
541
+
542
+ export default worker;
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowImportingTsExtensions": true,
4
+ "isolatedModules": true,
5
+ "jsx": "react-jsx",
6
+ "jsxImportSource": "preact",
7
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
8
+ "module": "preserve",
9
+ "moduleDetection": "force",
10
+ "moduleResolution": "bundler",
11
+ "noEmit": true,
12
+ "noFallthroughCasesInSwitch": true,
13
+ "noUncheckedSideEffectImports": true,
14
+ "noUnusedLocals": true,
15
+ "noUnusedParameters": true,
16
+ "skipLibCheck": true,
17
+ "strict": true,
18
+ "target": "es2022"
19
+ },
20
+ "include": ["./src"]
21
+ }