git-truck 0.8.2 → 0.8.6-experimental

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 (42) hide show
  1. package/.github/workflows/bump-version.yml +1 -1
  2. package/README.md +17 -13
  3. package/cli.js +2 -0
  4. package/dev.js +4 -2
  5. package/package.json +4 -4
  6. package/server.ts +13 -8
  7. package/src/analyzer/analyze.server.ts +43 -76
  8. package/src/analyzer/analyze.test.ts +30 -30
  9. package/src/analyzer/args.server.ts +20 -6
  10. package/src/analyzer/constants.ts +1 -1
  11. package/src/analyzer/git-caller.server.ts +290 -0
  12. package/src/analyzer/hydrate.server.ts +1 -1
  13. package/src/analyzer/model.ts +13 -2
  14. package/src/analyzer/{util.ts → util.server.ts} +27 -33
  15. package/src/analyzer/util.test.ts +1 -1
  16. package/src/components/AnalyzingIndicator.tsx +55 -0
  17. package/src/components/Animations.ts +14 -0
  18. package/src/components/Chart.tsx +29 -8
  19. package/src/components/Details.tsx +8 -7
  20. package/src/components/GlobalInfo.tsx +19 -8
  21. package/src/components/HiddenFiles.tsx +3 -3
  22. package/src/components/Legend.tsx +1 -1
  23. package/src/components/LegendOther.tsx +42 -42
  24. package/src/components/Main.tsx +1 -1
  25. package/src/components/SearchBar.tsx +1 -6
  26. package/src/components/util.tsx +19 -10
  27. package/src/const.ts +6 -6
  28. package/src/contexts/ClickedContext.ts +17 -17
  29. package/src/contexts/DataContext.ts +12 -12
  30. package/src/contexts/MetricContext.ts +12 -12
  31. package/src/contexts/OptionsContext.ts +51 -51
  32. package/src/contexts/SearchContext.ts +19 -19
  33. package/src/lang-map.d.ts +3 -3
  34. package/src/metrics.ts +3 -2
  35. package/src/root.tsx +44 -1
  36. package/src/routes/{repo.tsx → $repo.tsx} +59 -15
  37. package/src/routes/index.tsx +156 -46
  38. package/build/index.js +0 -6836
  39. package/post-build.js +0 -14
  40. package/public/favicon.ico +0 -0
  41. package/src/analyzer/git-caller.ts +0 -117
  42. package/src/analyzer/index.ts +0 -4
@@ -1,11 +1,12 @@
1
1
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
2
- import { faRotate } from "@fortawesome/free-solid-svg-icons"
3
- import { Form, useTransition } from "remix"
2
+ import { faRotate as reanalyzeIcon, faLeftLong as backIcon } from "@fortawesome/free-solid-svg-icons"
3
+ import { Form, Link, useTransition } from "remix"
4
4
  import { dateTimeFormatShort } from "~/util"
5
5
  import { useData } from "../contexts/DataContext"
6
6
  import { usePath } from "../contexts/PathContext"
7
7
  import { Spacer } from "./Spacer"
8
- import { Box, BoxTitle, InlineCode, TextButton } from "./util"
8
+ import { Box, BoxTitle, Code, TextButton } from "./util"
9
+ import styled from "styled-components"
9
10
 
10
11
  export function GlobalInfo() {
11
12
  const data = useData()
@@ -31,26 +32,36 @@ export function GlobalInfo() {
31
32
 
32
33
  return (
33
34
  <Box>
35
+ <StyledLink to=".." title="See all projects">
36
+ <FontAwesomeIcon icon={backIcon} color="#333" />
37
+ </StyledLink>
34
38
  <BoxTitle>{data.repo}</BoxTitle>
35
39
  <Spacer />
36
40
  <div>
37
41
  <strong>Branch: </strong>
38
- {data.branch}
42
+ <span>{data.branch}</span>
39
43
  <Spacer />
40
44
  <strong>Analyzed: </strong>
41
- {dateTimeFormatShort(data.lastRunEpoch)}
45
+ <span>{dateTimeFormatShort(data.lastRunEpoch)}</span>
42
46
  <Spacer />
43
47
  <strong>As of commit: </strong>
44
- <InlineCode title={data.commit.message ?? "No commit message"}>{data.commit.hash.slice(0, 7)}</InlineCode>
48
+ <Code inline title={data.commit.message ?? "No commit message"}>
49
+ {data.commit.hash.slice(0, 7)}
50
+ </Code>
45
51
  </div>
46
52
  <Spacer />
47
- <Form method="post" action="/repo">
53
+ <Form method="post" action=".">
48
54
  <input type="hidden" name="refresh" value="true" />
49
55
  <TextButton disabled={transitionState.state !== "idle"}>
50
- <FontAwesomeIcon icon={faRotate} />{" "}
56
+ <FontAwesomeIcon icon={reanalyzeIcon} />{" "}
51
57
  {!transitionState.submission?.formData.has("refresh") ? "Rerun analyzer" : "Analyzing..."}
52
58
  </TextButton>
53
59
  </Form>
54
60
  </Box>
55
61
  )
56
62
  }
63
+
64
+ const StyledLink = styled(Link)`
65
+ display: inline-flex;
66
+ margin-right: var(--unit);
67
+ `
@@ -4,7 +4,7 @@ import { useBoolean } from "react-use"
4
4
  import styled from "styled-components"
5
5
  import { useData } from "~/contexts/DataContext"
6
6
  import { ExpandUp } from "./Toggle"
7
- import { Box, BoxSubTitle, InlineCode } from "./util"
7
+ import { Box, BoxSubTitle, Code } from "./util"
8
8
  import { Form, useTransition } from "remix"
9
9
 
10
10
  const Line = styled.div`
@@ -67,14 +67,14 @@ export function HiddenFiles() {
67
67
  <div>
68
68
  {data.hiddenFiles.map((hidden) => (
69
69
  <Line key={hidden} title={hidden}>
70
- <InlineForm method="post" action="/repo">
70
+ <InlineForm method="post" action=".">
71
71
  <input type="hidden" name="unignore" value={hidden} />
72
72
  <StyledButton title="Show file" disabled={transitionState.state !== "idle"}>
73
73
  <StyledFontAwesomeIcon id="eyeslash" icon={faEyeSlash} />
74
74
  <StyledFontAwesomeIcon id="eye" icon={faEye} />
75
75
  </StyledButton>
76
76
  </InlineForm>
77
- <InlineCode>{hiddenFileFormat(hidden)}</InlineCode>
77
+ <Code>{hiddenFileFormat(hidden)}</Code>
78
78
  </Line>
79
79
  ))}
80
80
  </div>
@@ -33,7 +33,7 @@ const GradArrow = styled.i<{ visible: boolean; position: number }>`
33
33
  position: relative;
34
34
  bottom: 11px;
35
35
  left: calc(${({ position }) => position * 100}% - ${estimatedLetterWidth}px);
36
- filter: drop-shadow(0px -2px 0.5px #fff);
36
+ filter: drop-shadow(0px -2px 0px #fff);
37
37
  `
38
38
 
39
39
  const StyledBox = styled(Box)`
@@ -1,42 +1,42 @@
1
- import styled from "styled-components"
2
- import { PointInfo } from "../metrics"
3
- import { Spacer } from "./Spacer"
4
- import { LegendDot, LegendEntry, LegendLabel } from "./util"
5
-
6
- interface LegendOtherProps {
7
- toggle: () => void
8
- items: [string, PointInfo][]
9
- show: boolean
10
- }
11
-
12
- const LegendOtherDiv = styled.div`
13
- width: fit-content;
14
- &:hover {
15
- cursor: pointer;
16
- }
17
- `
18
-
19
- export function LegendOther(props: LegendOtherProps) {
20
- if (!props.show) return null
21
-
22
- return (
23
- <LegendOtherDiv>
24
- <LegendEntry onClick={props.toggle}>
25
- {props.items.slice(0, 14).map(([label, info], i) => {
26
- const margin = i === 0 ? 0 : -10
27
- return (
28
- <LegendDot
29
- key={`dot${label}`}
30
- dotColor={info.color}
31
- style={{
32
- marginLeft: margin,
33
- }}
34
- />
35
- )
36
- })}
37
- <Spacer horizontal />
38
- <LegendLabel>{props.items.length} more</LegendLabel>
39
- </LegendEntry>
40
- </LegendOtherDiv>
41
- )
42
- }
1
+ import styled from "styled-components"
2
+ import { PointInfo } from "../metrics"
3
+ import { Spacer } from "./Spacer"
4
+ import { LegendDot, LegendEntry, LegendLabel } from "./util"
5
+
6
+ interface LegendOtherProps {
7
+ toggle: () => void
8
+ items: [string, PointInfo][]
9
+ show: boolean
10
+ }
11
+
12
+ const LegendOtherDiv = styled.div`
13
+ width: fit-content;
14
+ &:hover {
15
+ cursor: pointer;
16
+ }
17
+ `
18
+
19
+ export function LegendOther(props: LegendOtherProps) {
20
+ if (!props.show) return null
21
+
22
+ return (
23
+ <LegendOtherDiv>
24
+ <LegendEntry onClick={props.toggle}>
25
+ {props.items.slice(0, 14).map(([label, info], i) => {
26
+ const margin = i === 0 ? 0 : -10
27
+ return (
28
+ <LegendDot
29
+ key={`dot${label}`}
30
+ dotColor={info.color}
31
+ style={{
32
+ marginLeft: margin,
33
+ }}
34
+ />
35
+ )
36
+ })}
37
+ <Spacer horizontal />
38
+ <LegendLabel>{props.items.length} more</LegendLabel>
39
+ </LegendEntry>
40
+ </LegendOtherDiv>
41
+ )
42
+ }
@@ -92,7 +92,7 @@ export function Main() {
92
92
  })}
93
93
  </Breadcrumb>
94
94
  ) : (
95
- <Breadcrumb></Breadcrumb>
95
+ <Breadcrumb />
96
96
  )}
97
97
  <ChartWrapper ref={ref}>
98
98
  <Chart size={size} />
@@ -1,4 +1,4 @@
1
- import { SearchField, Box, Label, StyledP, SearchResultButton, SearchResultSpan } from "./util"
1
+ import { SearchField, Box, Label, StyledP, SearchResultButton, SearchResultSpan, LightFontAwesomeIcon } from "./util"
2
2
  import styled from "styled-components"
3
3
  import { Fragment, useEffect, useRef, useState } from "react"
4
4
  import { useDebounce } from "react-use"
@@ -10,7 +10,6 @@ import { useData } from "~/contexts/DataContext"
10
10
  import { usePath } from "~/contexts/PathContext"
11
11
  import { useClickedObject } from "~/contexts/ClickedContext"
12
12
  import { allExceptLast, getSeparator } from "~/util"
13
- import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
14
13
  import { faFolderOpen, faFile } from "@fortawesome/free-solid-svg-icons"
15
14
 
16
15
  const StyledBox = styled(Box)`
@@ -35,10 +34,6 @@ function findSearchResults(tree: HydratedGitTreeObject, searchString: string) {
35
34
  return searchResults
36
35
  }
37
36
 
38
- const LightFontAwesomeIcon = styled(FontAwesomeIcon)`
39
- opacity: 0.5;
40
- `
41
-
42
37
  export default function SearchBar() {
43
38
  const searchFieldRef = useRef<HTMLInputElement>(null)
44
39
  const { searchText, setSearchText, searchResults, setSearchResults } = useSearch()
@@ -1,22 +1,25 @@
1
- import styled from "styled-components"
1
+ import styled, { css } from "styled-components"
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
2
3
 
3
- export const BoxTitle = styled.h2`
4
- font-size: 1.5em;
4
+ const titleBaseStyles = css`
5
5
  font-weight: bold;
6
6
  margin-bottom: 0;
7
7
  margin-top: 0;
8
8
  color: var(--title-color);
9
- overflow: hidden;
10
9
  text-overflow: ellipsis;
10
+ word-break: keep-all;
11
11
  white-space: nowrap;
12
+ overflow-x: hidden;
13
+ `
14
+
15
+ export const BoxTitle = styled.h2`
16
+ ${titleBaseStyles}
17
+ font-size: 1.5em;
12
18
  `
13
19
 
14
20
  export const BoxSubTitle = styled.h2`
21
+ ${titleBaseStyles}
15
22
  font-size: 1em;
16
- font-weight: bold;
17
- margin-bottom: 0;
18
- margin-top: 0;
19
- color: var(--title-color);
20
23
  `
21
24
 
22
25
  export const StyledP = styled.p`
@@ -25,6 +28,10 @@ export const StyledP = styled.p`
25
28
  margin: 0.5em 0 0.5em 0;
26
29
  `
27
30
 
31
+ export const LightFontAwesomeIcon = styled(FontAwesomeIcon)`
32
+ opacity: 0.5;
33
+ `
34
+
28
35
  export const TextButton = styled.button`
29
36
  background: var(--button-bg);
30
37
  width: fit-content;
@@ -166,9 +173,11 @@ export const DetailsValue = styled.p`
166
173
  font-size: 0.9em;
167
174
  `
168
175
 
169
- export const InlineCode = styled.code`
170
- display: inline-block;
176
+ export const Code = styled.code<{ inline?: boolean }>`
177
+ display: ${(props) => (props.inline ? "inline-block" : "block")};
171
178
  font-family: monospace;
179
+ font-size: 1.2em;
180
+ white-space: pre;
172
181
  `
173
182
 
174
183
  export const Grower = styled.div`
package/src/const.ts CHANGED
@@ -1,6 +1,6 @@
1
- export const treemapPadding = 20
2
- export const bubblePadding = 10
3
- export const textSpacingFromCircle = 0
4
- export const searchMatchColor = "red"
5
- export const estimatedLetterWidth = 7
6
- export const EstimatedLetterHeightForDirText = 14
1
+ export const treemapPadding = 20
2
+ export const bubblePadding = 10
3
+ export const textSpacingFromCircle = 0
4
+ export const searchMatchColor = "red"
5
+ export const estimatedLetterWidth = 7
6
+ export const EstimatedLetterHeightForDirText = 14
@@ -1,17 +1,17 @@
1
- import { createContext, Dispatch, SetStateAction, useContext } from "react"
2
- import { HydratedGitObject } from "../analyzer/model"
3
-
4
- export interface clickedObject {
5
- clickedObject: HydratedGitObject | null
6
- setClickedObject: Dispatch<SetStateAction<HydratedGitObject | null>>
7
- }
8
-
9
- export const ClickedObjectContext = createContext<clickedObject | null>(null)
10
-
11
- export function useClickedObject() {
12
- const context = useContext(ClickedObjectContext)
13
- if (!context) {
14
- throw new Error("useClickedObject must be used within a ClickedObjectProvider")
15
- }
16
- return context
17
- }
1
+ import { createContext, Dispatch, SetStateAction, useContext } from "react"
2
+ import { HydratedGitObject } from "../analyzer/model"
3
+
4
+ export interface clickedObject {
5
+ clickedObject: HydratedGitObject | null
6
+ setClickedObject: Dispatch<SetStateAction<HydratedGitObject | null>>
7
+ }
8
+
9
+ export const ClickedObjectContext = createContext<clickedObject | null>(null)
10
+
11
+ export function useClickedObject() {
12
+ const context = useContext(ClickedObjectContext)
13
+ if (!context) {
14
+ throw new Error("useClickedObject must be used within a ClickedObjectProvider")
15
+ }
16
+ return context
17
+ }
@@ -1,12 +1,12 @@
1
- import { createContext, useContext } from "react"
2
- import { AnalyzerData } from "~/analyzer/model"
3
-
4
- export const DataContext = createContext<AnalyzerData | undefined>(undefined)
5
-
6
- export function useData() {
7
- const context = useContext(DataContext)
8
- if (!context) {
9
- throw new Error("useData must be used within a DataContext")
10
- }
11
- return context
12
- }
1
+ import { createContext, useContext } from "react"
2
+ import { AnalyzerData } from "~/analyzer/model"
3
+
4
+ export const DataContext = createContext<AnalyzerData | undefined>(undefined)
5
+
6
+ export function useData() {
7
+ const context = useContext(DataContext)
8
+ if (!context) {
9
+ throw new Error("useData must be used within a DataContext")
10
+ }
11
+ return context
12
+ }
@@ -1,12 +1,12 @@
1
- import { createContext, useContext } from "react"
2
- import { MetricsData } from "../metrics"
3
-
4
- export const MetricsContext = createContext<MetricsData | undefined>(undefined)
5
-
6
- export function useMetrics() {
7
- const context = useContext(MetricsContext)
8
- if (!context) {
9
- throw new Error("useMetrics must be used within a MetricsContext")
10
- }
11
- return context
12
- }
1
+ import { createContext, useContext } from "react"
2
+ import { MetricsData } from "../metrics"
3
+
4
+ export const MetricsContext = createContext<MetricsData | undefined>(undefined)
5
+
6
+ export function useMetrics() {
7
+ const context = useContext(MetricsContext)
8
+ if (!context) {
9
+ throw new Error("useMetrics must be used within a MetricsContext")
10
+ }
11
+ return context
12
+ }
@@ -1,51 +1,51 @@
1
- import { createContext, useContext } from "react"
2
- import { Authorship, AuthorshipType, Metric, MetricType } from "../metrics"
3
-
4
- export const Chart = {
5
- BUBBLE_CHART: "Bubble chart",
6
- TREE_MAP: "Tree map",
7
- }
8
-
9
- export type ChartType = keyof typeof Chart
10
-
11
- export interface Options {
12
- metricType: MetricType
13
- chartType: ChartType
14
- authorshipType: AuthorshipType
15
- animationsEnabled: boolean
16
- setMetricType: (metricType: MetricType) => void
17
- setChartType: (chartType: ChartType) => void
18
- setAuthorshipType: (authorshipType: AuthorshipType) => void
19
- setAnimationsEnabled: (animationsEnabled: boolean) => void
20
- }
21
-
22
- export const OptionsContext = createContext<Options | undefined>(undefined)
23
-
24
- export function useOptions() {
25
- const context = useContext(OptionsContext)
26
- if (!context) {
27
- throw new Error("useSearch must be used within a SearchProvider")
28
- }
29
- return context
30
- }
31
-
32
- export function getDefaultOptions(): Options {
33
- return {
34
- metricType: Object.keys(Metric)[0] as MetricType,
35
- chartType: Object.keys(Chart)[0] as ChartType,
36
- authorshipType: Object.keys(Authorship)[0] as AuthorshipType,
37
- animationsEnabled: true,
38
- setChartType: () => {
39
- throw new Error("No chartTypeSetter provided")
40
- },
41
- setMetricType: () => {
42
- throw new Error("No metricTypeSetter provided")
43
- },
44
- setAuthorshipType: () => {
45
- throw new Error("No AuthorshipTypeSetter provided")
46
- },
47
- setAnimationsEnabled: () => {
48
- throw new Error("No animationsEnabledSetter provided")
49
- },
50
- }
51
- }
1
+ import { createContext, useContext } from "react"
2
+ import { Authorship, AuthorshipType, Metric, MetricType } from "../metrics"
3
+
4
+ export const Chart = {
5
+ BUBBLE_CHART: "Bubble chart",
6
+ TREE_MAP: "Tree map",
7
+ }
8
+
9
+ export type ChartType = keyof typeof Chart
10
+
11
+ export interface Options {
12
+ metricType: MetricType
13
+ chartType: ChartType
14
+ authorshipType: AuthorshipType
15
+ animationsEnabled: boolean
16
+ setMetricType: (metricType: MetricType) => void
17
+ setChartType: (chartType: ChartType) => void
18
+ setAuthorshipType: (authorshipType: AuthorshipType) => void
19
+ setAnimationsEnabled: (animationsEnabled: boolean) => void
20
+ }
21
+
22
+ export const OptionsContext = createContext<Options | undefined>(undefined)
23
+
24
+ export function useOptions() {
25
+ const context = useContext(OptionsContext)
26
+ if (!context) {
27
+ throw new Error("useSearch must be used within a SearchProvider")
28
+ }
29
+ return context
30
+ }
31
+
32
+ export function getDefaultOptions(): Options {
33
+ return {
34
+ metricType: Object.keys(Metric)[0] as MetricType,
35
+ chartType: Object.keys(Chart)[0] as ChartType,
36
+ authorshipType: Object.keys(Authorship)[0] as AuthorshipType,
37
+ animationsEnabled: true,
38
+ setChartType: () => {
39
+ throw new Error("No chartTypeSetter provided")
40
+ },
41
+ setMetricType: () => {
42
+ throw new Error("No metricTypeSetter provided")
43
+ },
44
+ setAuthorshipType: () => {
45
+ throw new Error("No AuthorshipTypeSetter provided")
46
+ },
47
+ setAnimationsEnabled: () => {
48
+ throw new Error("No animationsEnabledSetter provided")
49
+ },
50
+ }
51
+ }
@@ -1,19 +1,19 @@
1
- import { createContext, Dispatch, SetStateAction, useContext } from "react"
2
- import { HydratedGitObject } from "~/analyzer/model"
3
-
4
- type Search = {
5
- searchText: string
6
- setSearchText: Dispatch<SetStateAction<string>>
7
- searchResults: HydratedGitObject[]
8
- setSearchResults: Dispatch<SetStateAction<HydratedGitObject[]>>
9
- }
10
-
11
- export const SearchContext = createContext<Search | undefined>(undefined)
12
-
13
- export function useSearch(): Search {
14
- const context = useContext(SearchContext)
15
- if (!context) {
16
- throw new Error("useSearch must be used within a SearchProvider")
17
- }
18
- return context
19
- }
1
+ import { createContext, Dispatch, SetStateAction, useContext } from "react"
2
+ import { HydratedGitObject } from "~/analyzer/model"
3
+
4
+ type Search = {
5
+ searchText: string
6
+ setSearchText: Dispatch<SetStateAction<string>>
7
+ searchResults: HydratedGitObject[]
8
+ setSearchResults: Dispatch<SetStateAction<HydratedGitObject[]>>
9
+ }
10
+
11
+ export const SearchContext = createContext<Search | undefined>(undefined)
12
+
13
+ export function useSearch(): Search {
14
+ const context = useContext(SearchContext)
15
+ if (!context) {
16
+ throw new Error("useSearch must be used within a SearchProvider")
17
+ }
18
+ return context
19
+ }
package/src/lang-map.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare module "lang-map" {
2
- export function languages(extension: string): string[]
3
- }
1
+ declare module "lang-map" {
2
+ export function languages(extension: string): string[]
3
+ }
package/src/metrics.ts CHANGED
@@ -276,8 +276,10 @@ function setDominanceColor(blob: HydratedGitBlobObject, cache: MetricCache, auth
276
276
  const defaultColor = "hsl(210, 38%, 85%)"
277
277
  const nocreditColor = "teal"
278
278
 
279
+ const authorUnion = blob.unionedAuthors?.get(authorshipType) ?? {}
280
+
279
281
  let creditsum = 0
280
- for (const [, val] of Object.entries(blob.authors)) {
282
+ for (const [, val] of Object.entries(authorUnion)) {
281
283
  creditsum += val
282
284
  }
283
285
 
@@ -289,7 +291,6 @@ function setDominanceColor(blob: HydratedGitBlobObject, cache: MetricCache, auth
289
291
  return
290
292
  }
291
293
 
292
- const authorUnion = blob.unionedAuthors?.get(authorshipType)
293
294
 
294
295
  if (!authorUnion) throw Error("No unioned authors found")
295
296
  switch (Object.keys(authorUnion).length) {
package/src/root.tsx CHANGED
@@ -1,10 +1,12 @@
1
- import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from "remix"
1
+ import { ErrorBoundaryComponent, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useCatch } from "remix"
2
2
  import type { MetaFunction } from "remix"
3
3
 
4
4
  import appStyles from "~/styles/App.css"
5
5
  import varsStyles from "~/styles/vars.css"
6
6
  import indexStyles from "~/styles/index.css"
7
7
  import chartStyles from "~/styles/Chart.css"
8
+ import { useEffect } from "react"
9
+ import { Code } from "./components/util"
8
10
 
9
11
  export const meta: MetaFunction = () => {
10
12
  return { title: "Git Truck 🚛" }
@@ -36,3 +38,44 @@ export default function App() {
36
38
  </html>
37
39
  )
38
40
  }
41
+
42
+ export function CatchBoundary() {
43
+ const caught = useCatch()
44
+
45
+ return (
46
+ <html>
47
+ <head>
48
+ <title>Oops! An error wasn't handled</title>
49
+ <Meta />
50
+ <Links />
51
+ </head>
52
+ <body>
53
+ <h1>
54
+ {caught.status} {caught.statusText}
55
+ </h1>
56
+ <Scripts />
57
+ </body>
58
+ </html>
59
+ )
60
+ }
61
+
62
+ export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
63
+ useEffect(() => {
64
+ console.error(error.message)
65
+ }, [error])
66
+
67
+ return (
68
+ <html>
69
+ <head>
70
+ <title>Oops! An error wasn't handled</title>
71
+ <Meta />
72
+ <Links />
73
+ </head>
74
+ <body>
75
+ <h1>{error.message}</h1>
76
+ <Code>{error.stack}</Code>
77
+ <Scripts />
78
+ </body>
79
+ </html>
80
+ )
81
+ }