@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,67 @@
1
+ /** HMR update event types */
2
+ export type HmrEventType = "update" | "full-reload" | "css-update" | "connected";
3
+
4
+ /** SSE controller for a connected client */
5
+ interface SseClient {
6
+ controller: ReadableStreamDefaultController<Uint8Array>;
7
+ encoder: TextEncoder;
8
+ }
9
+
10
+ const hmrClients = new Set<SseClient>();
11
+
12
+ /** Checks if request is an HMR SSE request. */
13
+ export function isHmrRequest(request: Request) {
14
+ const url = new URL(request.url);
15
+ return url.pathname === "/_hmr" && request.method === "GET";
16
+ }
17
+
18
+ /** Handles HMR SSE request. */
19
+ export function handleHmrRequest() {
20
+ const encoder = new TextEncoder();
21
+ let client: SseClient;
22
+ let heartbeatInterval: ReturnType<typeof setInterval>;
23
+
24
+ const stream = new ReadableStream<Uint8Array>({
25
+ start(controller) {
26
+ client = { controller, encoder };
27
+ hmrClients.add(client);
28
+
29
+ const data = JSON.stringify({ type: "connected" });
30
+ controller.enqueue(encoder.encode(`data: ${data}\n\n`));
31
+
32
+ heartbeatInterval = setInterval(() => {
33
+ try {
34
+ controller.enqueue(encoder.encode(`: heartbeat\n\n`));
35
+ } catch {
36
+ clearInterval(heartbeatInterval);
37
+ hmrClients.delete(client);
38
+ }
39
+ }, 3_000);
40
+ },
41
+ cancel() {
42
+ clearInterval(heartbeatInterval);
43
+ hmrClients.delete(client);
44
+ },
45
+ });
46
+
47
+ return new Response(stream, {
48
+ headers: {
49
+ "Cache-Control": "no-cache",
50
+ Connection: "keep-alive",
51
+ "Content-Encoding": "identity",
52
+ "Content-Type": "text/event-stream",
53
+ },
54
+ });
55
+ }
56
+
57
+ /** Broadcasts an HMR update to all connected clients. */
58
+ export function broadcastHmrUpdate(type: HmrEventType, path?: string) {
59
+ const message = JSON.stringify({ type, path, timestamp: Date.now() });
60
+ for (const client of hmrClients) {
61
+ try {
62
+ client.controller.enqueue(client.encoder.encode(`data: ${message}\n\n`));
63
+ } catch {
64
+ hmrClients.delete(client);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,42 @@
1
+ import { peekRuntime } from "./runtime.ts";
2
+ import { escapeJsonForHtml } from "./serialize.ts";
3
+
4
+ /** Generates inline script to preload stylesheets. */
5
+ export function generateStylePreloadScript(stylesheets: Array<{ id: string; css: string }>) {
6
+ if (stylesheets.length === 0) return "";
7
+
8
+ return /* tsx */ `
9
+ <script type="application/json" id="sf-preloaded-styles">
10
+ ${escapeJsonForHtml(stylesheets)}
11
+ </script>
12
+ <script>
13
+ (function() {
14
+ if (!('adoptedStyleSheets' in Document.prototype)) return;
15
+ var data = JSON.parse(document.getElementById('sf-preloaded-styles').textContent);
16
+ var g = globalThis.__solarflare__ = globalThis.__solarflare__ || {};
17
+ g.preloadedStyles = new Map();
18
+ data.forEach(function(s) {
19
+ var sheet = new CSSStyleSheet();
20
+ sheet.replaceSync(s.css);
21
+ g.preloadedStyles.set(s.id, sheet);
22
+ });
23
+ })();
24
+ </script>
25
+ `;
26
+ }
27
+
28
+ /** Retrieves preloaded stylesheets. */
29
+ export function getPreloadedStylesheet(id: string) {
30
+ const runtime = peekRuntime();
31
+ return runtime?.preloadedStyles?.get(id) ?? null;
32
+ }
33
+
34
+ /** Hydrates preloaded stylesheets into the manager. */
35
+ export function hydratePreloadedStyles(_manager: {
36
+ register: (id: string, css: string, opts?: any) => CSSStyleSheet | null;
37
+ }) {
38
+ const preloaded = peekRuntime()?.preloadedStyles;
39
+ if (!preloaded) return;
40
+
41
+ console.log(`[styles] Hydrated ${preloaded.size} preloaded stylesheets`);
42
+ }
package/src/server.ts ADDED
@@ -0,0 +1,480 @@
1
+ import { type FunctionComponent, type VNode, h } from "preact";
2
+ import { renderToReadableStream, type RenderStream } from "preact-render-to-string/stream";
3
+ import { parsePath } from "./paths.ts";
4
+ import { escapeJsonForHtml } from "./serialize.ts";
5
+ import { initStore, setPathname, resetStore } from "./store.ts";
6
+ import {
7
+ serializeStoreForHydration,
8
+ serializeDataIsland,
9
+ getDeferredIslandId,
10
+ getHydrateScriptId,
11
+ } from "./hydration.ts";
12
+ import { BODY_MARKER, createAssetInjectionTransformer } from "./stream-assets.ts";
13
+ import {
14
+ createHeadContext,
15
+ setHeadContext,
16
+ resetHeadContext,
17
+ installHeadHoisting,
18
+ resetHeadElementTracking,
19
+ } from "./head.ts";
20
+
21
+ export { Head, useHead } from "./head.ts";
22
+
23
+ /** Body placeholder component for layout injection. */
24
+ export function Body() {
25
+ return h("solarflare-body", {
26
+ dangerouslySetInnerHTML: { __html: BODY_MARKER },
27
+ });
28
+ }
29
+
30
+ /** Route parameter definition. */
31
+ export interface RouteParamDef {
32
+ name: string;
33
+ optional: boolean;
34
+ segment: string;
35
+ }
36
+
37
+ /** Parsed route pattern. */
38
+ export interface ParsedPattern {
39
+ filePath: string;
40
+ pathname: string;
41
+ params: RouteParamDef[];
42
+ isStatic: boolean;
43
+ specificity: number;
44
+ }
45
+
46
+ /** Route definition. */
47
+ export interface Route {
48
+ pattern: URLPattern;
49
+ parsedPattern: ParsedPattern;
50
+ path: string;
51
+ tag: string;
52
+ loader: () => Promise<{ default: unknown }>;
53
+ type: "client" | "server";
54
+ }
55
+
56
+ /** Converts file path to URLPattern with parsed metadata. */
57
+ export function parsePattern(filePath: string) {
58
+ const parsed = parsePath(filePath);
59
+
60
+ // Transform params from string[] to RouteParamDef[]
61
+ const params: RouteParamDef[] = parsed.params.map((name) => ({
62
+ name,
63
+ optional: false,
64
+ segment: `:${name}`,
65
+ }));
66
+
67
+ return {
68
+ filePath: parsed.original,
69
+ pathname: parsed.pattern,
70
+ params,
71
+ isStatic: params.length === 0,
72
+ specificity: parsed.specificity,
73
+ };
74
+ }
75
+
76
+ /** Structured module map with typed categories. */
77
+ export interface ModuleMap {
78
+ server: Record<string, () => Promise<{ default: unknown }>>;
79
+ client: Record<string, () => Promise<{ default: unknown }>>;
80
+ layout: Record<string, () => Promise<{ default: unknown }>>;
81
+ error?: () => Promise<{ default: unknown }>;
82
+ }
83
+
84
+ /** Creates router from module map, returning sorted routes array. */
85
+ export function createRouter(modules: ModuleMap) {
86
+ const routeModules = { ...modules.server, ...modules.client };
87
+
88
+ const routes = Object.entries(routeModules)
89
+ .filter(([path]) => !path.includes("/_"))
90
+ .map(([path, loader]) => {
91
+ const parsedPattern = parsePattern(path);
92
+ return {
93
+ pattern: new URLPattern({ pathname: parsedPattern.pathname }),
94
+ parsedPattern,
95
+ path,
96
+ tag: parsePath(path).tag,
97
+ loader,
98
+ type: path.includes(".server.") ? ("server" as const) : ("client" as const),
99
+ };
100
+ })
101
+ .sort((a, b) => {
102
+ if (a.parsedPattern.isStatic !== b.parsedPattern.isStatic) {
103
+ return a.parsedPattern.isStatic ? -1 : 1;
104
+ }
105
+ return b.parsedPattern.specificity - a.parsedPattern.specificity;
106
+ });
107
+
108
+ return routes;
109
+ }
110
+
111
+ /** Layout definition. */
112
+ export interface Layout {
113
+ path: string;
114
+ loader: () => Promise<{ default: unknown }>;
115
+ depth: number;
116
+ directory: string;
117
+ }
118
+
119
+ /** Layout hierarchy result. */
120
+ export interface LayoutHierarchy {
121
+ layouts: Layout[];
122
+ segments: string[];
123
+ checkedPaths: string[];
124
+ }
125
+
126
+ /** Finds all ancestor layouts for a route path, root to leaf order. */
127
+ export function findLayoutHierarchy(
128
+ routePath: string,
129
+ modules: Record<string, () => Promise<{ default: unknown }>>,
130
+ ): LayoutHierarchy {
131
+ const layouts: Layout[] = [];
132
+ const checkedPaths: string[] = [];
133
+
134
+ const segments = routePath.replace(/^\.\//, "").split("/").slice(0, -1);
135
+ const rootLayout = "./_layout.tsx";
136
+ checkedPaths.push(rootLayout);
137
+ if (rootLayout in modules) {
138
+ layouts.push({
139
+ path: rootLayout,
140
+ loader: modules[rootLayout],
141
+ depth: 0,
142
+ directory: ".",
143
+ });
144
+ }
145
+ let current = ".";
146
+ for (let i = 0; i < segments.length; i++) {
147
+ const segment = segments[i];
148
+ if (!segment) continue;
149
+ current += `/${segment}`;
150
+ const layoutPath = `${current}/_layout.tsx`;
151
+ checkedPaths.push(layoutPath);
152
+ if (layoutPath in modules) {
153
+ layouts.push({
154
+ path: layoutPath,
155
+ loader: modules[layoutPath],
156
+ depth: i + 1,
157
+ directory: current,
158
+ });
159
+ }
160
+ }
161
+
162
+ return { layouts, segments, checkedPaths };
163
+ }
164
+
165
+ /** Finds ancestor layouts for a route using structured module map. */
166
+ export function findLayouts(routePath: string, modules: ModuleMap) {
167
+ return findLayoutHierarchy(routePath, modules.layout).layouts;
168
+ }
169
+
170
+ /** Route match result. */
171
+ export interface RouteMatch {
172
+ route: Route;
173
+ params: Record<string, string>;
174
+ paramDefs: RouteParamDef[];
175
+ complete: boolean;
176
+ }
177
+
178
+ /** Matches URL against routes using URLPattern. */
179
+ export function matchRoute(routes: Route[], url: URL) {
180
+ for (const route of routes) {
181
+ const result = route.pattern.exec(url);
182
+ if (result) {
183
+ const params = (result.pathname.groups as Record<string, string>) ?? {};
184
+ const paramDefs = route.parsedPattern.params;
185
+
186
+ const complete = paramDefs
187
+ .filter((p) => !p.optional)
188
+ .every((p) => p.name in params && params[p.name] !== undefined);
189
+
190
+ return {
191
+ route,
192
+ params,
193
+ paramDefs,
194
+ complete,
195
+ };
196
+ }
197
+ }
198
+ return null;
199
+ }
200
+
201
+ /** Layout props interface. */
202
+ export interface LayoutProps {
203
+ children: VNode<any>;
204
+ }
205
+
206
+ /** Wraps content in nested layouts (root to leaf order). */
207
+ export async function wrapWithLayouts(content: VNode<any>, layouts: Layout[]) {
208
+ let wrapped: VNode<any> = content;
209
+
210
+ for (let i = layouts.length - 1; i >= 0; i--) {
211
+ const { loader } = layouts[i];
212
+ const mod = await loader();
213
+ const Layout = mod.default as FunctionComponent<LayoutProps>;
214
+ wrapped = h(Layout, { children: wrapped });
215
+ }
216
+
217
+ return wrapped;
218
+ }
219
+
220
+ /** Renders a component with its tag wrapper for hydration. */
221
+ export function renderComponent(
222
+ Component: FunctionComponent<any>,
223
+ tag: string,
224
+ props: Record<string, unknown>,
225
+ ) {
226
+ const attrs: Record<string, string> = {};
227
+ for (const [key, value] of Object.entries(props)) {
228
+ const type = typeof value;
229
+ if (type === "string" || type === "number" || type === "boolean") {
230
+ attrs[key] = String(value);
231
+ }
232
+ }
233
+ return h(tag, attrs, h(Component, props));
234
+ }
235
+
236
+ /** Error page props interface. */
237
+ export interface ErrorPageProps {
238
+ error: Error;
239
+ url?: URL;
240
+ statusCode?: number;
241
+ reset?: () => void;
242
+ }
243
+
244
+ /** Renders an error page wrapped in layouts. */
245
+ export async function renderErrorPage(
246
+ error: Error,
247
+ url: URL,
248
+ modules: ModuleMap,
249
+ statusCode = 500,
250
+ ) {
251
+ let ErrorComponent: FunctionComponent<ErrorPageProps>;
252
+ if (modules.error) {
253
+ const mod = await modules.error();
254
+ ErrorComponent = mod.default as FunctionComponent<ErrorPageProps>;
255
+ } else {
256
+ ErrorComponent = ({ error, url, statusCode }: ErrorPageProps) =>
257
+ h(
258
+ "div",
259
+ { class: "error-page" },
260
+ h("h1", null, statusCode === 404 ? "Not Found" : "Something went wrong"),
261
+ h("p", null, error.message),
262
+ url && h("p", { class: "error-url" }, `Failed to load: ${url.pathname}`),
263
+ h("a", { href: "/" }, "Go home"),
264
+ );
265
+ }
266
+
267
+ const errorContent = h(ErrorComponent, { error, url, statusCode });
268
+
269
+ const layouts = findLayoutHierarchy("./_error.tsx", modules.layout).layouts;
270
+ if (layouts.length > 0) {
271
+ return wrapWithLayouts(errorContent, layouts);
272
+ }
273
+
274
+ return errorContent;
275
+ }
276
+
277
+ /** Deferred data configuration for streaming. */
278
+ export interface DeferredData {
279
+ /** Component tag to hydrate. */
280
+ tag: string;
281
+ /** Multiple independent deferred props, streamed as each promise resolves. */
282
+ promises: Record<string, Promise<unknown>>;
283
+ }
284
+
285
+ /** Streaming render options. */
286
+ export interface StreamRenderOptions {
287
+ /** Route parameters. */
288
+ params?: Record<string, string>;
289
+ /** Server-loaded data for immediate render. */
290
+ serverData?: unknown;
291
+ /** Current pathname. */
292
+ pathname?: string;
293
+ /** Script path to inject. */
294
+ script?: string;
295
+ /** Stylesheet paths. */
296
+ styles?: string[];
297
+ /** Dev scripts (e.g., console forwarding). */
298
+ devScripts?: string[];
299
+ /** Deferred data to stream after shell. */
300
+ deferred?: DeferredData;
301
+ /** HTTP status code to return. */
302
+ _status?: number;
303
+ /** HTTP status text to return. */
304
+ _statusText?: string;
305
+ /** Custom HTTP headers to merge. */
306
+ _headers?: Record<string, string>;
307
+ }
308
+
309
+ /** Initializes server-side store with request context. */
310
+ export function initServerContext(options: StreamRenderOptions) {
311
+ resetStore();
312
+ resetHeadContext();
313
+ resetHeadElementTracking();
314
+
315
+ // Install head hoisting (idempotent - only installs once)
316
+ installHeadHoisting();
317
+
318
+ // Create fresh head context for this request
319
+ const headCtx = createHeadContext();
320
+ setHeadContext(headCtx);
321
+
322
+ initStore({
323
+ params: options.params,
324
+ serverData: options.serverData,
325
+ });
326
+
327
+ if (options.pathname) {
328
+ setPathname(options.pathname);
329
+ }
330
+ }
331
+
332
+ /** Extended stream interface with allReady promise. */
333
+ export interface SolarflareStream extends ReadableStream<Uint8Array> {
334
+ /** Resolves when all content has been rendered. */
335
+ allReady: Promise<void>;
336
+ /** HTTP status code. */
337
+ status?: number;
338
+ /** HTTP status text. */
339
+ statusText?: string;
340
+ /** Custom HTTP headers. */
341
+ headers?: Record<string, string>;
342
+ }
343
+
344
+ /** Renders a VNode to a streaming response with asset injection. */
345
+ export async function renderToStream(vnode: VNode<any>, options: StreamRenderOptions = {}) {
346
+ initServerContext(options);
347
+
348
+ const storeScript = await serializeStoreForHydration();
349
+ const stream = renderToReadableStream(vnode) as RenderStream;
350
+
351
+ const transformer = createAssetInjectionTransformer(
352
+ storeScript,
353
+ options.script,
354
+ options.styles,
355
+ options.devScripts,
356
+ );
357
+
358
+ const transformedStream = stream.pipeThrough(transformer);
359
+
360
+ if (options.deferred) {
361
+ const resultStream = createDeferredStream(transformedStream, options.deferred);
362
+ (resultStream as SolarflareStream).allReady = stream.allReady;
363
+ (resultStream as SolarflareStream).status = options._status ?? 200;
364
+ (resultStream as SolarflareStream).statusText = options._statusText;
365
+ (resultStream as SolarflareStream).headers = options._headers;
366
+ return resultStream as SolarflareStream;
367
+ }
368
+
369
+ const resultStream = transformedStream as SolarflareStream;
370
+ resultStream.allReady = stream.allReady;
371
+ resultStream.status = options._status ?? 200;
372
+ resultStream.statusText = options._statusText;
373
+ resultStream.headers = options._headers;
374
+ return resultStream;
375
+ }
376
+
377
+ /** Creates stream that flushes HTML immediately, then appends deferred data. */
378
+ function createDeferredStream(inputStream: ReadableStream<Uint8Array>, deferred: DeferredData) {
379
+ const encoder = new TextEncoder();
380
+
381
+ let controller: ReadableStreamDefaultController<Uint8Array>;
382
+
383
+ const tag = deferred.tag;
384
+
385
+ let inputDone = false;
386
+ let pendingDeferred = 0;
387
+ let allowDeferredFlush = true;
388
+ let closed = false;
389
+ const pendingChunks: Uint8Array[] = [];
390
+ const emittedDeferred = new Set<string>();
391
+
392
+ function maybeClose() {
393
+ if (closed) return;
394
+ if (!inputDone) return;
395
+ if (pendingDeferred !== 0) return;
396
+ if (!allowDeferredFlush) return;
397
+ closed = true;
398
+ controller.close();
399
+ }
400
+
401
+ function flushPendingChunks() {
402
+ if (!allowDeferredFlush) return;
403
+
404
+ while (pendingChunks.length > 0) {
405
+ controller.enqueue(pendingChunks.shift()!);
406
+ }
407
+
408
+ maybeClose();
409
+ }
410
+
411
+ function enqueueDeferredChunk(html: string) {
412
+ const chunk = encoder.encode(html);
413
+ if (allowDeferredFlush) {
414
+ controller.enqueue(chunk);
415
+ maybeClose();
416
+ } else {
417
+ pendingChunks.push(chunk);
418
+ }
419
+ }
420
+
421
+ /** Builds data island HTML with deferred hydration trigger. */
422
+ async function buildDeferredHtml(dataIslandId: string, data: unknown, hydrateScriptId: string) {
423
+ const dataIsland = await serializeDataIsland(dataIslandId, data);
424
+ const hydrationDetail = escapeJsonForHtml({ tag, id: dataIslandId });
425
+ const hydrationScript = /* html */ `<script id="${hydrateScriptId}">(function(){var s=document.currentScript;setTimeout(()=>{document.dispatchEvent(new CustomEvent("sf:queue-hydrate",{detail:${hydrationDetail}}));s?.remove();},0);})()</script>`;
426
+ return dataIsland + hydrationScript;
427
+ }
428
+
429
+ const stream = new ReadableStream<Uint8Array>({
430
+ start(ctrl) {
431
+ controller = ctrl;
432
+
433
+ void (async () => {
434
+ const reader = inputStream.getReader();
435
+
436
+ try {
437
+ while (true) {
438
+ const { done, value } = await reader.read();
439
+ if (done) break;
440
+ controller.enqueue(value);
441
+ }
442
+ } catch (err) {
443
+ controller.error(err);
444
+ return;
445
+ }
446
+
447
+ inputDone = true;
448
+ flushPendingChunks();
449
+ maybeClose();
450
+ })();
451
+
452
+ const entries = Object.entries(deferred.promises);
453
+ pendingDeferred = entries.length;
454
+
455
+ entries.forEach(([key, promise]) => {
456
+ void Promise.resolve(promise)
457
+ .then(async (value) => {
458
+ const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
459
+ const dataIslandId = getDeferredIslandId(tag, safeKey);
460
+ const hydrateScriptId = getHydrateScriptId(tag, safeKey);
461
+ if (emittedDeferred.has(dataIslandId) || emittedDeferred.has(hydrateScriptId)) return;
462
+ emittedDeferred.add(dataIslandId);
463
+ emittedDeferred.add(hydrateScriptId);
464
+ const html = await buildDeferredHtml(dataIslandId, { [key]: value }, hydrateScriptId);
465
+ enqueueDeferredChunk(html);
466
+ })
467
+ .catch((err) => {
468
+ const errorScript = /* html */ `<script>console.error("[solarflare] Deferred error (${escapeJsonForHtml(key)}):", ${escapeJsonForHtml((err as Error).message)})</script>`;
469
+ enqueueDeferredChunk(errorScript);
470
+ })
471
+ .finally(() => {
472
+ pendingDeferred--;
473
+ maybeClose();
474
+ });
475
+ });
476
+ },
477
+ });
478
+
479
+ return stream;
480
+ }
@@ -0,0 +1,101 @@
1
+ interface ImportMeta {
2
+ glob<T = { default: unknown }>(
3
+ pattern: string,
4
+ options?: { eager?: boolean },
5
+ ): Record<string, () => Promise<T>>;
6
+ /** The file path of the current module (Node runtime) */
7
+ path?: string;
8
+ /** Environment variables (bundler) */
9
+ env?: {
10
+ DEV?: boolean;
11
+ PROD?: boolean;
12
+ MODE?: string;
13
+ [key: string]: unknown;
14
+ };
15
+ }
16
+
17
+ declare module "*.css" {
18
+ const classNames: Record<string, string>;
19
+ export default classNames;
20
+ }
21
+
22
+ declare module "*.gif" {
23
+ const image: string;
24
+ export default image;
25
+ }
26
+
27
+ declare module "*.html" {
28
+ const html: string;
29
+ export default html;
30
+ }
31
+
32
+ declare module "*.ico" {
33
+ const image: string;
34
+ export default image;
35
+ }
36
+
37
+ declare module "*.jpeg" {
38
+ const image: string;
39
+ export default image;
40
+ }
41
+
42
+ declare module "*.jpg" {
43
+ const image: string;
44
+ export default image;
45
+ }
46
+
47
+ declare module "*.png" {
48
+ const image: string;
49
+ export default image;
50
+ }
51
+
52
+ declare module "*.svg" {
53
+ const image: any;
54
+ export default image;
55
+ }
56
+
57
+ /**
58
+ * Solarflare Framework Types
59
+ */
60
+ declare module "@chr33s/solarflare/client" {
61
+ import { FunctionComponent, VNode } from "preact";
62
+
63
+ export type RenderPriority = "critical" | "high" | "normal" | "low" | "idle";
64
+
65
+ export function Deferred(props: {
66
+ priority?: RenderPriority;
67
+ fallback?: VNode;
68
+ children: VNode;
69
+ }): VNode;
70
+
71
+ export interface DefineOptions {
72
+ tag?: string;
73
+ shadow?: boolean;
74
+ observedAttributes?: string[];
75
+ validate?: boolean;
76
+ }
77
+
78
+ export function define<P extends Record<string, any>>(
79
+ Component: FunctionComponent<P>,
80
+ options?: DefineOptions,
81
+ ): FunctionComponent<P>;
82
+
83
+ export interface NavigateOptions {
84
+ replace?: boolean;
85
+ state?: unknown;
86
+ skipTransition?: boolean;
87
+ }
88
+
89
+ export function navigate(to: string | URL, options?: NavigateOptions): Promise<void>;
90
+ }
91
+
92
+ declare module "@chr33s/solarflare/server" {
93
+ import { VNode } from "preact";
94
+
95
+ export function Body(): VNode<any>;
96
+ export function Head(): VNode<any>;
97
+ }
98
+
99
+ declare module "@chr33s/solarflare" {
100
+ export default function worker(request: Request, env: Env): Promise<Response>;
101
+ }