@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
@@ -0,0 +1,254 @@
1
+ /** Log levels matching wrangler's --log-level options. */
2
+ export type LogLevel = "debug" | "info" | "log" | "warn" | "error" | "none";
3
+
4
+ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
5
+ debug: 0,
6
+ info: 1,
7
+ log: 2,
8
+ warn: 3,
9
+ error: 4,
10
+ none: 5,
11
+ };
12
+
13
+ /** Checks if a log level should be shown given the threshold. */
14
+ function shouldLog(level: string, threshold: LogLevel) {
15
+ const levelPriority = LOG_LEVEL_PRIORITY[level as LogLevel] ?? LOG_LEVEL_PRIORITY.log;
16
+ const thresholdPriority = LOG_LEVEL_PRIORITY[threshold];
17
+ return levelPriority >= thresholdPriority;
18
+ }
19
+
20
+ /** Log entry from the browser. */
21
+ interface LogEntry {
22
+ level: string;
23
+ message: string;
24
+ timestamp: string;
25
+ url?: string;
26
+ stacks?: string[];
27
+ extra?: unknown[];
28
+ }
29
+
30
+ /** Request payload from client. */
31
+ interface ClientLogRequest {
32
+ logs: LogEntry[];
33
+ }
34
+
35
+ /** Console forwarding configuration. */
36
+ export interface ConsoleForwardOptions {
37
+ enabled?: boolean;
38
+ endpoint?: string;
39
+ levels?: ("log" | "warn" | "error" | "info" | "debug")[];
40
+ includeStacks?: boolean;
41
+ }
42
+
43
+ const DEFAULT_OPTIONS: Required<ConsoleForwardOptions> = {
44
+ enabled: true,
45
+ endpoint: "/_console",
46
+ levels: ["log", "warn", "error", "info", "debug"],
47
+ includeStacks: true,
48
+ };
49
+
50
+ /** ANSI color codes. */
51
+ const colors = {
52
+ reset: "\x1b[0m",
53
+ dim: "\x1b[2m",
54
+ red: "\x1b[31m",
55
+ yellow: "\x1b[33m",
56
+ blue: "\x1b[34m",
57
+ cyan: "\x1b[36m",
58
+ gray: "\x1b[90m",
59
+ };
60
+
61
+ /** Gets ANSI color code for log level. */
62
+ function getLevelColor(level: string) {
63
+ switch (level) {
64
+ case "error":
65
+ return colors.red;
66
+ case "warn":
67
+ return colors.yellow;
68
+ case "info":
69
+ return colors.cyan;
70
+ case "debug":
71
+ return colors.gray;
72
+ default:
73
+ return colors.blue;
74
+ }
75
+ }
76
+
77
+ /** Formats log message for terminal output. */
78
+ function formatLogMessage(log: LogEntry) {
79
+ const color = getLevelColor(log.level);
80
+ const prefix = `${color}[browser:${log.level}]${colors.reset}`;
81
+ let message = `${prefix} ${log.message}`;
82
+
83
+ if (log.stacks && log.stacks.length > 0) {
84
+ message +=
85
+ "\n" +
86
+ log.stacks
87
+ .map((stack) =>
88
+ stack
89
+ .split("\n")
90
+ .map((line) => `${colors.dim} ${line}${colors.reset}`)
91
+ .join("\n"),
92
+ )
93
+ .join("\n");
94
+ }
95
+
96
+ if (log.extra && log.extra.length > 0) {
97
+ const extraStr = JSON.stringify(log.extra, null, 2);
98
+ message +=
99
+ "\n" +
100
+ extraStr
101
+ .split("\n")
102
+ .map((line) => `${colors.dim} ${line}${colors.reset}`)
103
+ .join("\n");
104
+ }
105
+
106
+ return message;
107
+ }
108
+
109
+ /** Processes console logs from the client. */
110
+ export async function processConsoleLogs(request: Request, logLevel: LogLevel = "log") {
111
+ try {
112
+ const { logs }: ClientLogRequest = await request.json();
113
+
114
+ for (const log of logs) {
115
+ // Filter logs based on the configured log level
116
+ if (shouldLog(log.level, logLevel)) {
117
+ console.log(formatLogMessage(log));
118
+ }
119
+ }
120
+
121
+ return new Response(JSON.stringify({ success: true }), {
122
+ status: 200,
123
+ headers: { "Content-Type": "application/json" },
124
+ });
125
+ } catch (error) {
126
+ console.error("[browser:error] Failed to process console logs:", error);
127
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), {
128
+ status: 400,
129
+ headers: { "Content-Type": "application/json" },
130
+ });
131
+ }
132
+ }
133
+
134
+ /** Checks if request is a console forward request. */
135
+ export function isConsoleRequest(request: Request, options: ConsoleForwardOptions = {}) {
136
+ const { endpoint } = { ...DEFAULT_OPTIONS, ...options };
137
+ const url = new URL(request.url);
138
+ return url.pathname === endpoint && request.method === "POST";
139
+ }
140
+
141
+ /** Generates client-side script that patches console methods. */
142
+ export function generateClientScript(options: ConsoleForwardOptions = {}) {
143
+ const { endpoint, levels, includeStacks } = {
144
+ ...DEFAULT_OPTIONS,
145
+ ...options,
146
+ };
147
+
148
+ return /* ts */ `
149
+ (function() {
150
+ const originalMethods = {
151
+ log: console.log.bind(console),
152
+ warn: console.warn.bind(console),
153
+ error: console.error.bind(console),
154
+ info: console.info.bind(console),
155
+ debug: console.debug.bind(console),
156
+ };
157
+
158
+ const logBuffer = [];
159
+ let flushTimeout = null;
160
+ const FLUSH_DELAY = 100;
161
+ const MAX_BUFFER_SIZE = 50;
162
+
163
+ function createLogEntry(level, args) {
164
+ const stacks = [];
165
+ const extra = [];
166
+
167
+ const message = Array.from(args).map((arg) => {
168
+ if (arg === undefined) return "undefined";
169
+ if (arg === null) return "null";
170
+ if (typeof arg === "string") return arg;
171
+ if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
172
+
173
+ if (arg instanceof Error || (arg && typeof arg.stack === "string")) {
174
+ let stringifiedError = arg.toString();
175
+ if (${includeStacks} && arg.stack) {
176
+ let stack = arg.stack.toString();
177
+ if (stack.startsWith(stringifiedError)) {
178
+ stack = stack.slice(stringifiedError.length).trimStart();
179
+ }
180
+ if (stack) {
181
+ stacks.push(stack);
182
+ }
183
+ }
184
+ return stringifiedError;
185
+ }
186
+
187
+ if (typeof arg === "object") {
188
+ try {
189
+ const serialized = JSON.parse(JSON.stringify(arg));
190
+ extra.push(serialized);
191
+ return "[object]";
192
+ } catch {
193
+ return String(arg);
194
+ }
195
+ }
196
+ return String(arg);
197
+ }).join(" ");
198
+
199
+ return {
200
+ level,
201
+ message,
202
+ timestamp: new Date().toISOString(),
203
+ url: window.location.href,
204
+ stacks,
205
+ extra: extra.length > 0 ? extra : undefined,
206
+ };
207
+ }
208
+
209
+ async function sendLogs(logs) {
210
+ const payload = JSON.stringify({ logs });
211
+ const blob = new Blob([payload], { type: "application/json" });
212
+ navigator.sendBeacon("${endpoint}", blob);
213
+ }
214
+
215
+ function flushLogs() {
216
+ if (logBuffer.length === 0) return;
217
+ const logsToSend = [...logBuffer];
218
+ logBuffer.length = 0;
219
+ sendLogs(logsToSend);
220
+ if (flushTimeout) {
221
+ clearTimeout(flushTimeout);
222
+ flushTimeout = null;
223
+ }
224
+ }
225
+
226
+ function addToBuffer(entry) {
227
+ logBuffer.push(entry);
228
+ if (logBuffer.length >= MAX_BUFFER_SIZE) {
229
+ flushLogs();
230
+ return;
231
+ }
232
+ if (!flushTimeout) {
233
+ flushTimeout = setTimeout(flushLogs, FLUSH_DELAY);
234
+ }
235
+ }
236
+
237
+ // Patch console methods
238
+ ${levels
239
+ .map(
240
+ (level) => /* ts */ `
241
+ console.${level} = function(...args) {
242
+ originalMethods.${level}(...args);
243
+ const entry = createLogEntry("${level}", args);
244
+ addToBuffer(entry);
245
+ };
246
+ `,
247
+ )
248
+ .join("\n")}
249
+
250
+ window.addEventListener("beforeunload", flushLogs);
251
+ setInterval(flushLogs, 10000);
252
+ })();
253
+ `;
254
+ }
@@ -0,0 +1,103 @@
1
+ import { createHash } from "node:crypto";
2
+ import { escapeJsonForHtml } from "./serialize.ts";
3
+
4
+ /** Critical CSS cache entry. */
5
+ interface CriticalCssEntry {
6
+ css: string;
7
+ hash: string;
8
+ timestamp: number;
9
+ }
10
+
11
+ /** In-memory cache for critical CSS (per route). */
12
+ const criticalCssCache = new Map<string, CriticalCssEntry>();
13
+
14
+ /** Max age for cached critical CSS (1 hour). */
15
+ const CACHE_MAX_AGE = 60 * 60 * 1000;
16
+
17
+ /** Extracts critical CSS for a route */
18
+ export async function extractCriticalCss(
19
+ routePattern: string,
20
+ cssFiles: string[],
21
+ options: {
22
+ readCss: (path: string) => Promise<string>;
23
+ maxSize?: number;
24
+ cache?: boolean;
25
+ },
26
+ ) {
27
+ const cacheKey = routePattern;
28
+ const maxSize = options.maxSize ?? 14 * 1024;
29
+
30
+ if (options.cache !== false) {
31
+ const cached = criticalCssCache.get(cacheKey);
32
+ if (cached && Date.now() - cached.timestamp < CACHE_MAX_AGE) {
33
+ return cached.css;
34
+ }
35
+ }
36
+
37
+ const cssContents: string[] = [];
38
+ let totalSize = 0;
39
+
40
+ for (const file of cssFiles) {
41
+ try {
42
+ const content = await options.readCss(file);
43
+ const minified = minifyCss(content);
44
+
45
+ if (totalSize + minified.length > maxSize) {
46
+ break;
47
+ }
48
+
49
+ cssContents.push(minified);
50
+ totalSize += minified.length;
51
+ } catch {}
52
+ }
53
+
54
+ const criticalCss = cssContents.join("\n");
55
+ const hash = createHash("md5").update(criticalCss).digest("hex").slice(0, 8);
56
+
57
+ if (options.cache !== false) {
58
+ criticalCssCache.set(cacheKey, {
59
+ css: criticalCss,
60
+ hash,
61
+ timestamp: Date.now(),
62
+ });
63
+ }
64
+
65
+ return criticalCss;
66
+ }
67
+
68
+ /** Simple CSS minification for critical CSS */
69
+ function minifyCss(css: string) {
70
+ return css
71
+ .replace(/\/\*[\s\S]*?\*\//g, "")
72
+ .replace(/\s+/g, " ")
73
+ .replace(/\s*([{}: ;,])\s*/g, "$1")
74
+ .trim();
75
+ }
76
+
77
+ /** Generates a noscript fallback for CSS loading. */
78
+ export function generateCssFallback(stylesheets: string[]) {
79
+ const links = stylesheets
80
+ .map((href) => /* html */ `<link rel="stylesheet" href="${href}">`)
81
+ .join("");
82
+ return /* html */ `<noscript>${links}</noscript>`;
83
+ }
84
+
85
+ /** Generates async CSS loading script without blocking render on non-critical CSS */
86
+ export function generateAsyncCssLoader(stylesheets: string[]) {
87
+ if (stylesheets.length === 0) return "";
88
+
89
+ const hrefs = escapeJsonForHtml(stylesheets);
90
+
91
+ return /* html */ `
92
+ <script>
93
+ (function() {
94
+ var ss=${hrefs};
95
+ ss.forEach(function(h){
96
+ var l=document.createElement('link');
97
+ l.rel='stylesheet';l.href=h;
98
+ document.head.appendChild(l);
99
+ });
100
+ })();
101
+ </script>
102
+ `;
103
+ }
@@ -0,0 +1,52 @@
1
+ const ENDPOINT = "/.well-known/appspecific/com.chrome.devtools.json";
2
+
3
+ export interface DevToolsJSON {
4
+ workspace: {
5
+ root: string;
6
+ uuid: string;
7
+ };
8
+ }
9
+
10
+ export interface DevToolsOptions {
11
+ projectRoot?: string;
12
+ uuid?: string;
13
+ }
14
+
15
+ let cachedUuid: string | null = null;
16
+
17
+ export function setDevToolsUuid(uuid: string) {
18
+ cachedUuid = uuid;
19
+ }
20
+
21
+ function getOrCreateUuid(providedUuid?: string) {
22
+ if (providedUuid) return providedUuid;
23
+ if (cachedUuid) return cachedUuid;
24
+ cachedUuid = crypto.randomUUID();
25
+ return cachedUuid;
26
+ }
27
+
28
+ export function isDevToolsRequest(request: Request) {
29
+ const url = new URL(request.url);
30
+ return url.pathname === ENDPOINT && request.method === "GET";
31
+ }
32
+
33
+ /** Handles devtools.json request. Returns the project settings JSON. */
34
+ export function handleDevToolsRequest(options: DevToolsOptions = {}) {
35
+ const root = options.projectRoot ?? (typeof process !== "undefined" ? process.cwd() : "/");
36
+ const uuid = getOrCreateUuid(options.uuid);
37
+
38
+ const devtoolsJson: DevToolsJSON = {
39
+ workspace: {
40
+ root,
41
+ uuid,
42
+ },
43
+ };
44
+
45
+ return new Response(JSON.stringify(devtoolsJson, null, 2), {
46
+ status: 200,
47
+ headers: {
48
+ "Content-Type": "application/json",
49
+ "Cache-Control": "no-cache",
50
+ },
51
+ });
52
+ }