@solidjs/start 0.4.10 → 0.5.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.
@@ -1,59 +1,25 @@
1
1
  import {
2
- defineMiddleware,
3
- getRequestIP,
4
2
  H3Event,
5
- sendWebResponse,
6
- toWebRequest
3
+ defineMiddleware,
4
+ sendWebResponse
7
5
  } from "vinxi/server";
8
- import { FetchEvent } from "./types";
9
-
10
- const h3EventSymbol = Symbol("h3Event");
11
- const fetchEventSymbol = Symbol("fetchEvent");
12
- const eventTraps = {
13
- get(target, prop) {
14
- if (prop === fetchEventSymbol) return target;
15
- return target[prop] ?? target[h3EventSymbol][prop];
16
- }
17
- };
18
-
19
- export function createFetchEvent(event: H3Event): FetchEvent {
20
- const request = toWebRequest(event);
21
- return new Proxy(
22
- {
23
- request: request,
24
- clientAddress: getRequestIP(event),
25
- locals: {},
26
- // @ts-ignore
27
- [h3EventSymbol]: event
28
- },
29
- eventTraps
30
- ) as unknown as FetchEvent;
31
- }
32
-
33
- export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
34
- return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
35
- }
36
-
37
- export function getFetchEvent(h3Event: H3Event): FetchEvent {
38
- if (!h3Event[fetchEventSymbol]) {
39
- const fetchEvent = createFetchEvent(h3Event);
40
- h3Event[fetchEventSymbol] = fetchEvent;
41
- // @ts-ignore
42
- }
43
-
44
- return h3Event[fetchEventSymbol];
45
- }
6
+ import { getFetchEvent } from "../server/fetchEvent";
7
+ import type { FetchEvent } from "../server/types";
46
8
 
47
9
  /** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
48
10
 
49
11
  export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
50
12
  /** This composes an array of Exchanges into a single ExchangeIO function */
51
13
 
52
- type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
14
+ export type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
15
+
16
+ // copy-pasted from h3/dist/index.d.ts
17
+ type EventHandlerResponse<T = any> = T | Promise<T>;
18
+ type ResponseMiddlewareResponseParam = { body?: Awaited<EventHandlerResponse> };
53
19
 
54
- type ResponseMiddleware = (
20
+ export type ResponseMiddleware = (
55
21
  event: FetchEvent,
56
- response: Response
22
+ response: ResponseMiddlewareResponseParam
57
23
  ) => Response | Promise<Response> | void | Promise<void>;
58
24
 
59
25
  function wrapRequestMiddleware(onRequest: RequestMiddleware) {
@@ -69,7 +35,7 @@ function wrapRequestMiddleware(onRequest: RequestMiddleware) {
69
35
  }
70
36
 
71
37
  function wrapResponseMiddleware(onBeforeResponse: ResponseMiddleware) {
72
- return async (h3Event: H3Event, response: Response) => {
38
+ return async (h3Event: H3Event, response: ResponseMiddlewareResponseParam) => {
73
39
  const fetchEvent = getFetchEvent(h3Event);
74
40
  const mwResponse = await onBeforeResponse(fetchEvent, response);
75
41
  if (!mwResponse) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidjs/start",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "author": "Ryan Carniato",
6
6
  "publishConfig": {
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  "client",
11
11
  "config",
12
- "entry",
12
+ "middleware",
13
13
  "server",
14
14
  "shared",
15
15
  "*.d.ts"
@@ -18,8 +18,7 @@
18
18
  "exports": {
19
19
  ".": "./shared/index.tsx",
20
20
  "./config": "./config/index.js",
21
- "./entry": "./entry/index.ts",
22
- "./entry/spa": "./entry/spa/index.ts",
21
+ "./middleware": "./middleware/index.ts",
23
22
  "./server": "./server/index.tsx",
24
23
  "./server/spa": "./server/spa/index.tsx",
25
24
  "./client": "./client/index.tsx",
@@ -31,8 +30,8 @@
31
30
  ".": [
32
31
  "./shared/index.tsx"
33
32
  ],
34
- "./entry": [
35
- "./entry/index.ts"
33
+ "./middleware": [
34
+ "./middleware/index.ts"
36
35
  ],
37
36
  "./server": [
38
37
  "./server/index.tsx"
@@ -43,23 +42,24 @@
43
42
  }
44
43
  },
45
44
  "devDependencies": {
46
- "solid-js": "^1.8.11",
47
- "vinxi": "^0.1.4"
45
+ "solid-js": "^1.8.12",
46
+ "vinxi": "^0.2.1"
48
47
  },
49
48
  "dependencies": {
50
- "@vinxi/plugin-directives": "^0.1.2",
51
- "@vinxi/server-components": "^0.1.2",
52
- "@vinxi/server-functions": "^0.1.2",
49
+ "@vinxi/plugin-directives": "^0.2.0",
50
+ "@vinxi/server-components": "^0.2.0",
51
+ "@vinxi/server-functions": "^0.2.1",
53
52
  "defu": "^6.1.2",
54
53
  "error-stack-parser": "^2.1.4",
55
54
  "html-to-image": "^1.11.11",
55
+ "radix3": "^1.1.0",
56
56
  "seroval": "^1.0.2",
57
57
  "seroval-plugins": "^1.0.2",
58
58
  "shikiji": "^0.9.12",
59
59
  "source-map-js": "^1.0.2",
60
60
  "terracotta": "^1.0.4",
61
61
  "vite-plugin-inspect": "^0.7.33",
62
- "vite-plugin-solid": "^2.8.0"
62
+ "vite-plugin-solid": "^2.9.1"
63
63
  },
64
64
  "scripts": {
65
65
  "typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
@@ -35,9 +35,9 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
35
35
 
36
36
  let assets = [];
37
37
  Promise.resolve().then(async () => {
38
- if (context.routerMatches && context.routerMatches[0]) {
39
- const matches = [...context.routerMatches[0]];
40
- while (matches.length && (!matches[0].metadata || !matches[0].metadata.filesystem))
38
+ if (context.router && context.router.matches) {
39
+ const matches = [...context.router.matches];
40
+ while (matches.length && (!matches[0].info || !matches[0].info.filesystem))
41
41
  matches.shift();
42
42
  const matched = matches.length && matchRoute(matches, context.routes);
43
43
  if (matched) {
@@ -0,0 +1,115 @@
1
+ import {
2
+ H3Event,
3
+ HTTPEventSymbol,
4
+ appendResponseHeader,
5
+ getRequestIP,
6
+ getResponseHeader,
7
+ getResponseHeaders,
8
+ getResponseStatus,
9
+ getResponseStatusText,
10
+ removeResponseHeader,
11
+ setHeader,
12
+ setResponseHeader,
13
+ setResponseStatus,
14
+ toWebRequest
15
+ } from "vinxi/server";
16
+ import type { FetchEvent, ResponseStub } from "./types";
17
+
18
+ const fetchEventSymbol = Symbol("fetchEvent");
19
+
20
+ export function createFetchEvent(event: H3Event): FetchEvent {
21
+ const request = toWebRequest(event);
22
+ return {
23
+ request: request,
24
+ response: createResponseStub(event),
25
+ clientAddress: getRequestIP(event),
26
+ locals: {},
27
+ nativeEvent: event,
28
+ [HTTPEventSymbol]: event
29
+ };
30
+ }
31
+
32
+ export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
33
+ return {
34
+ ...fetchEvent,
35
+ [HTTPEventSymbol]: fetchEvent[HTTPEventSymbol]
36
+ };
37
+ }
38
+
39
+ export function getFetchEvent(h3Event: H3Event): FetchEvent {
40
+ if (!h3Event[fetchEventSymbol]) {
41
+ const fetchEvent = createFetchEvent(h3Event);
42
+ h3Event[fetchEventSymbol] = fetchEvent;
43
+ // @ts-ignore
44
+ }
45
+
46
+ return h3Event[fetchEventSymbol];
47
+ }
48
+
49
+ export function mergeResponseHeaders(h3Event, headers) {
50
+ for (const [key, value] of headers.entries()) {
51
+ setHeader(h3Event, key, value);
52
+ }
53
+ }
54
+
55
+ class HeaderProxy {
56
+ constructor(private event: H3Event) {}
57
+ get(key: string) {
58
+ const h = getResponseHeader(this.event, key);
59
+ return Array.isArray(h) ? h.join(", ") : h;
60
+ }
61
+ has(key: string) {
62
+ return this.get(key) !== undefined;
63
+ }
64
+ set(key: string, value: string) {
65
+ return setResponseHeader(this.event, key, value);
66
+ }
67
+ delete(key: string) {
68
+ return removeResponseHeader(this.event, key);
69
+ }
70
+ append(key: string, value: string) {
71
+ appendResponseHeader(this.event, key, value);
72
+ }
73
+ getSetCookie() {
74
+ const cookies = getResponseHeader(this.event, "Set-Cookie");
75
+ return Array.isArray(cookies) ? cookies : [cookies];
76
+ }
77
+ forEach(fn: (value: string, key: string, object: Headers) => void) {
78
+ return this.entries().forEach(([key, value]) => fn(value, key, this));
79
+ }
80
+ entries() {
81
+ return getResponseHeaders(this.event).map(([key, value]) => [
82
+ key,
83
+ Array.isArray(value) ? value.join(", ") : value
84
+ ]);
85
+ }
86
+ keys() {
87
+ return getResponseHeaders(this.event).map(([key]) => key);
88
+ }
89
+ values() {
90
+ return getResponseHeaders(this.event).map(([key, value]) =>
91
+ Array.isArray(value) ? value.join(", ") : value
92
+ );
93
+ }
94
+ [Symbol.iterator]() {
95
+ return this.entries()[Symbol.iterator]();
96
+ }
97
+ }
98
+
99
+ function createResponseStub(event: H3Event): ResponseStub {
100
+ return {
101
+ get status() {
102
+ return getResponseStatus(event);
103
+ },
104
+ set status(v) {
105
+ setResponseStatus(event, v);
106
+ },
107
+ get statusText() {
108
+ return getResponseStatusText(event);
109
+ },
110
+ set statusText(v) {
111
+ setResponseStatus(event, getResponseStatus(), v);
112
+ },
113
+ headers: new HeaderProxy(event)
114
+ };
115
+ }
@@ -1,21 +1,19 @@
1
1
  import { sharedConfig } from "solid-js";
2
- import { renderToStream } from "solid-js/web";
2
+ import { renderToStream, renderToString } from "solid-js/web";
3
3
  /* @ts-ignore */
4
4
  import { provideRequestEvent } from "solid-js/web/storage";
5
5
  import {
6
6
  EventHandlerObject,
7
7
  EventHandlerRequest,
8
8
  H3Event,
9
- HTTPMethod,
10
9
  eventHandler,
11
10
  sendRedirect,
12
11
  setHeader,
13
12
  setResponseStatus
14
13
  } from "vinxi/server";
15
- import { getFetchEvent } from "../server/middleware";
16
- import { createPageEvent } from "../server/page-event";
17
- import { APIEvent, FetchEvent, PageEvent } from "../server/types";
18
- import { matchAPIRoute } from "../shared/routes";
14
+ import { getFetchEvent } from "./fetchEvent";
15
+ import { createPageEvent } from "./pageEvent";
16
+ import { FetchEvent, PageEvent } from "./types";
19
17
 
20
18
  export function createBaseHandler(
21
19
  fn: (context: PageEvent) => unknown,
@@ -35,21 +33,21 @@ export function createBaseHandler(
35
33
  onBeforeResponse: options.onBeforeResponse,
36
34
  handler: (e: H3Event<EventHandlerRequest> & { startEvent: FetchEvent }) => {
37
35
  const event = getFetchEvent(e);
36
+ const mode = import.meta.env.START_SSR;
38
37
 
39
38
  return provideRequestEvent(event, async () => {
40
- // api
41
- const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
42
- if (match) {
43
- const mod = await match.handler.import();
44
- const fn = mod[event.request.method];
45
- (event as APIEvent).params = match.params;
46
- sharedConfig.context = { event } as any;
47
- return await fn(event);
48
- }
49
-
50
- // render stream
51
- const doAsync = import.meta.env.START_SSR === "async";
39
+ // render
52
40
  const context = await createPageEvent(event);
41
+ if (mode === "sync") {
42
+ const html = renderToString(() => {
43
+ (sharedConfig.context as any).event = context;
44
+ return fn(context);
45
+ }, options);
46
+ if (context.response && context.response.headers.get("Location")) {
47
+ return sendRedirect(event, context.response.headers.get("Location"));
48
+ }
49
+ return html;
50
+ }
53
51
  let cloned = { ...options };
54
52
  if (cloned.onCompleteAll) {
55
53
  const og = cloned.onCompleteAll;
@@ -72,7 +70,7 @@ export function createBaseHandler(
72
70
  if (context.response && context.response.headers.get("Location")) {
73
71
  return sendRedirect(event, context.response.headers.get("Location"));
74
72
  }
75
- if (doAsync) return stream;
73
+ if (mode === "async") return stream;
76
74
  // fix cloudflare streaming
77
75
  const { writable, readable } = new TransformStream();
78
76
  stream.pipeTo(writable);
package/server/index.tsx CHANGED
@@ -1,4 +1,4 @@
1
- export * from "vinxi/server";
2
1
  export { StartServer } from "./StartServer";
3
- export * from "./middleware";
2
+ export { createHandler } from "./handler";
4
3
  export * from "./types";
4
+
@@ -1,11 +1,6 @@
1
1
  import {
2
- appendResponseHeader,
3
2
  getCookie,
4
- getResponseHeader,
5
- removeResponseHeader,
6
- setCookie,
7
- setResponseHeader,
8
- setResponseStatus
3
+ setCookie
9
4
  } from "vinxi/server";
10
5
  import { createRoutes } from "../shared/FileRoutes";
11
6
  import { FetchEvent, PageEvent } from "./types";
@@ -27,7 +22,7 @@ function initFromFlash(ctx: FetchEvent) {
27
22
  export async function createPageEvent(ctx: FetchEvent) {
28
23
  const clientManifest = import.meta.env.MANIFEST["client"];
29
24
  const serverManifest = import.meta.env.MANIFEST["ssr"];
30
- setResponseHeader(ctx, "Content-Type", "text/html");
25
+ ctx.response.headers.set("Cache-Control", "no-cache");
31
26
  // const prevPath = ctx.request.headers.get("x-solid-referrer");
32
27
  // const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
33
28
  const pageEvent: PageEvent = Object.assign(ctx, {
@@ -40,36 +35,10 @@ export async function createPageEvent(ctx: FetchEvent) {
40
35
  )
41
36
  : [])
42
37
  ],
43
- initialSubmission: initFromFlash(ctx),
44
- routes: createRoutes(),
45
- components: {
46
- status: props => {
47
- setResponseStatus(ctx, props.code, props.text);
48
- return () => setResponseStatus(ctx, 200);
49
- },
50
- header: props => {
51
- if (props.append) {
52
- appendResponseHeader(ctx, props.name, props.value);
53
- } else {
54
- setResponseHeader(ctx, props.name, props.value);
55
- }
56
-
57
- return () => {
58
- const value = getResponseHeader(ctx, props.name);
59
-
60
- // Todo: review this logic still the same in H3
61
- // H3 supports arrays so its possible there is no need to split..
62
- // typeof check here is to guard but ultimately may be unneeded
63
- if (value && typeof value === "string") {
64
- const values = value.split(", ");
65
- const index = values.indexOf(props.value);
66
- index !== -1 && values.splice(index, 1);
67
- if (values.length) setResponseHeader(ctx, props.name, values.join(", "));
68
- else removeResponseHeader(ctx, props.name);
69
- }
70
- };
71
- }
38
+ router: {
39
+ submission: initFromFlash(ctx)
72
40
  },
41
+ routes: createRoutes(),
73
42
  // prevUrl: prevPath || "",
74
43
  // mutation: mutation,
75
44
  // $type: FETCH_EVENT,
@@ -1,5 +1,5 @@
1
- import { createBaseHandler } from "..";
2
- import { FetchEvent, PageEvent } from "../../server/types";
1
+ import { createBaseHandler } from "../handler";
2
+ import { FetchEvent, PageEvent } from "../types";
3
3
 
4
4
  export function createHandler(
5
5
  fn: (context: PageEvent) => unknown,
@@ -1,5 +1,4 @@
1
- export * from "vinxi/server";
2
- export * from "../middleware";
3
1
  export * from "../types";
4
2
  export { StartServer } from "./StartServer";
3
+ export { createHandler } from "./handler";
5
4
 
package/server/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "solid-js";
2
- import type { EventHandlerRequest, H3Event } from "vinxi/server";
2
+ import { HTTPEventSymbol, type EventHandlerRequest, type H3Event } from "vinxi/server";
3
3
 
4
4
  // export const FETCH_EVENT = "$FETCH";
5
5
 
@@ -16,10 +16,21 @@ export type ContextMatches = {
16
16
  params: unknown;
17
17
  };
18
18
 
19
- export interface FetchEvent extends H3Event<EventHandlerRequest> {
19
+ export interface ResponseStub {
20
+ status?: number;
21
+ statusText?: string;
22
+ headers: Headers;
23
+ }
24
+ export interface FetchEvent {
20
25
  request: Request;
26
+ response: ResponseStub;
21
27
  clientAddress: string;
22
- locals: Record<string, unknown>;
28
+ locals: RequestEventLocals;
29
+ nativeEvent: H3Event<EventHandlerRequest>;
30
+ [HTTPEventSymbol]: H3Event<EventHandlerRequest>;
31
+ }
32
+ export interface RequestEventLocals {
33
+ [key: string | symbol]: any;
23
34
  }
24
35
  export interface PageEvent extends FetchEvent {
25
36
  manifest: any;
@@ -29,7 +40,6 @@ export interface PageEvent extends FetchEvent {
29
40
  // $type: typeof FETCH_EVENT;
30
41
  $islands: Set<string>;
31
42
  // mutation: boolean;
32
- response?: Response;
33
43
  }
34
44
 
35
45
  export interface APIEvent extends FetchEvent {
@@ -10,8 +10,8 @@ export function createRoutes() {
10
10
  return {
11
11
  ...route,
12
12
  ...(route.$$route ? route.$$route.require().route : undefined),
13
- metadata: {
14
- ...(route.$$route ? route.$$route.require().route.metadata : {}),
13
+ info: {
14
+ ...(route.$$route ? route.$$route.require().route.info : {}),
15
15
  filesystem: true
16
16
  },
17
17
  component: lazyRoute(
package/shared/GET.ts ADDED
@@ -0,0 +1,5 @@
1
+ export function GET<T extends (...args: any[]) => any>(fn: T) {
2
+ return (...args: Parameters<T>) => {
3
+ return (fn as any).GET(...args) as ReturnType<T>;
4
+ };
5
+ }
@@ -4,7 +4,24 @@ import { getRequestEvent, isServer } from "solid-js/web";
4
4
  export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
5
5
  if (isServer) {
6
6
  const event = getRequestEvent();
7
- if (event) onCleanup((event as any).components.header(props));
7
+
8
+ if (props.append) event.response.headers.append(props.name, props.value);
9
+ else event.response.headers.set(props.name, props.value);
10
+
11
+ onCleanup(() => {
12
+ if (event.nativeEvent.handled) return;
13
+ const value = event.response.headers.get(props.name);
14
+ if (!value) return;
15
+ if (!value.includes(", ")) {
16
+ if (value === props.value) event.response.headers.delete(props.name);
17
+ return;
18
+ }
19
+ const values = value.split(", ");
20
+ const index = values.indexOf(props.value);
21
+ index !== -1 && values.splice(index, 1);
22
+ if (values.length) event.response.headers.set(props.name, values.join(","));
23
+ else event.response.headers.delete(props.name);
24
+ });
8
25
  }
9
26
 
10
27
  return null;
@@ -4,7 +4,9 @@ import { getRequestEvent, isServer } from "solid-js/web";
4
4
  export function HttpStatusCode(props: { code: number, text?: string }) {
5
5
  if (isServer) {
6
6
  const event = getRequestEvent();
7
- if (event) onCleanup((event as any).components.status(props));
7
+ event.response.status = props.code;
8
+ event.response.statusText = props.text;
9
+ onCleanup(() => !event.nativeEvent.handled && (event.response.status = 200));
8
10
  }
9
11
  return null;
10
12
  }
@@ -190,7 +190,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem
190
190
  if (current) {
191
191
  htmlToImage.toPng(current, {
192
192
  style: {
193
- transform: 'scale(1)',
193
+ transform: 'scale(0.75)',
194
194
  },
195
195
  }).then((url) => {
196
196
  download(url, 'start-screenshot.png');
@@ -216,59 +216,61 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem
216
216
  <Dialog class="dev-overlay" isOpen>
217
217
  <div>
218
218
  <DialogOverlay class="dev-overlay-background"/>
219
- <DialogPanel ref={setPanel} class="dev-overlay-panel">
220
- <div class="dev-overlay-navbar">
221
- <div class="dev-overlay-navbar-left">
222
- <div class="dev-overlay-version">
223
- <div>
224
- <SolidStartIcon title="Solid Start Version" />
225
- </div>
226
- <span>{info.version as string}</span>
227
- </div>
228
- <Show when={props.errors.length > 1}>
229
- <div class="dev-overlay-pagination">
230
- <button class="dev-overlay-button" onClick={goPrev} type="button">
231
- <ArrowLeftIcon title="Go Previous" />
232
- </button>
233
- <div class="dev-overlay-page-counter">
234
- {`${truncated()} of ${props.errors.length}`}
219
+ <DialogPanel ref={setPanel} class="dev-overlay-panel-container">
220
+ <div class="dev-overlay-panel">
221
+ <div class="dev-overlay-navbar">
222
+ <div class="dev-overlay-navbar-left">
223
+ <div class="dev-overlay-version">
224
+ <div>
225
+ <SolidStartIcon title="Solid Start Version" />
235
226
  </div>
236
- <button class="dev-overlay-button" onClick={goNext} type="button">
237
- <ArrowRightIcon title="Go Next" />
238
- </button>
227
+ <span>{info.version as string}</span>
239
228
  </div>
240
- </Show>
241
- </div>
242
- <div class="dev-overlay-controls">
243
- <button class="dev-overlay-button" onClick={redirectToGithub} type="button">
244
- <GithubIcon title="Create an issue thread on Github" />
245
- </button>
246
- <button class="dev-overlay-button" onClick={redirectToDiscord} type="button">
247
- <DiscordIcon title="Join our Discord Channel" />
248
- </button>
249
- <button class="dev-overlay-button" onClick={downloadScreenshot} type="button">
250
- <CameraIcon title="Capture Error Overlay" />
251
- </button>
252
- <button class="dev-overlay-button" onClick={toggleIsCompiled} type="button">
253
- <Show when={isCompiled()} fallback={(
254
- <ViewOriginalIcon title="View Original Source" />
255
- )}>
256
- <ViewCompiledIcon title="View Compiled Source" />
229
+ <Show when={props.errors.length > 1}>
230
+ <div class="dev-overlay-pagination">
231
+ <button class="dev-overlay-button" onClick={goPrev} type="button">
232
+ <ArrowLeftIcon title="Go Previous" />
233
+ </button>
234
+ <div class="dev-overlay-page-counter">
235
+ {`${truncated()} of ${props.errors.length}`}
236
+ </div>
237
+ <button class="dev-overlay-button" onClick={goNext} type="button">
238
+ <ArrowRightIcon title="Go Next" />
239
+ </button>
240
+ </div>
257
241
  </Show>
258
- </button>
259
- <button class="dev-overlay-button" onClick={props.resetError} type="button">
260
- <RefreshIcon title="Reset Error" />
261
- </button>
242
+ </div>
243
+ <div class="dev-overlay-controls">
244
+ <button class="dev-overlay-button" onClick={redirectToGithub} type="button">
245
+ <GithubIcon title="Create an issue thread on Github" />
246
+ </button>
247
+ <button class="dev-overlay-button" onClick={redirectToDiscord} type="button">
248
+ <DiscordIcon title="Join our Discord Channel" />
249
+ </button>
250
+ <button class="dev-overlay-button" onClick={downloadScreenshot} type="button">
251
+ <CameraIcon title="Capture Error Overlay" />
252
+ </button>
253
+ <button class="dev-overlay-button" onClick={toggleIsCompiled} type="button">
254
+ <Show when={isCompiled()} fallback={(
255
+ <ViewOriginalIcon title="View Original Source" />
256
+ )}>
257
+ <ViewCompiledIcon title="View Compiled Source" />
258
+ </Show>
259
+ </button>
260
+ <button class="dev-overlay-button" onClick={props.resetError} type="button">
261
+ <RefreshIcon title="Reset Error" />
262
+ </button>
263
+ </div>
262
264
  </div>
265
+ <Show when={props.errors[truncated() - 1]} keyed>
266
+ {(current) => (
267
+ <div class="dev-overlay-content">
268
+ <ErrorInfo error={current} />
269
+ <StackFrames error={current} isCompiled={isCompiled()} />
270
+ </div>
271
+ )}
272
+ </Show>
263
273
  </div>
264
- <Show when={props.errors[truncated() - 1]} keyed>
265
- {(current) => (
266
- <div class="dev-overlay-content">
267
- <ErrorInfo error={current} />
268
- <StackFrames error={current} isCompiled={isCompiled()} />
269
- </div>
270
- )}
271
- </Show>
272
274
  </DialogPanel>
273
275
  </div>
274
276
  </Dialog>