├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── assets └── screenshot.png ├── examples ├── latest │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── entry.client.tsx │ │ ├── entry.server.tsx │ │ ├── root.tsx │ │ └── routes │ │ │ ├── _remix-crash.tsx │ │ │ └── index.tsx │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── remix.config.js │ ├── remix.env.d.ts │ └── tsconfig.json └── v1.0.5 │ ├── .gitignore │ ├── README.md │ ├── api │ └── index.js │ ├── app │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── root.tsx │ ├── routes │ │ ├── _remix-crash.tsx │ │ ├── demos │ │ │ ├── about.tsx │ │ │ ├── about │ │ │ │ ├── index.tsx │ │ │ │ └── whoa.tsx │ │ │ ├── actions.tsx │ │ │ ├── correct.tsx │ │ │ ├── params.tsx │ │ │ └── params │ │ │ │ ├── $id.tsx │ │ │ │ └── index.tsx │ │ └── index.tsx │ └── styles │ │ ├── dark.css │ │ ├── demos │ │ ├── about.css │ │ └── remix.css │ │ └── global.css │ ├── package.json │ ├── public │ └── favicon.ico │ ├── remix.config.js │ ├── remix.env.d.ts │ ├── tsconfig.json │ └── vercel.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── postcss.config.js ├── rollup.config.js ├── src ├── client │ ├── ErrorBoundary.tsx │ ├── index.css │ └── index.ts ├── server │ ├── endpoints.ts │ ├── index.ts │ └── utils.ts └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug report' 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | This issue tracker is for reporting bugs found in `remix-crash` (https://github.com/xstevenyung/remix-crash). 10 | If you have a question about how to achieve something and are struggling, please post a question 11 | inside of `remix-crash` Discussions tab: https://github.com/xstevenyung/remix-crash/discussions 12 | 13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: 14 | - `remix-crash` Issues tab: https://github.com/xstevenyung/remix-crash/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 15 | - `remix-crash` closed issues tab: https://github.com/xstevenyung/remix-crash/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed 16 | - `remix-crash` Discussions tab: https://github.com/xstevenyung/remix-crash/discussions 17 | 18 | The more information you fill in, the better the community can help you. 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the bug 23 | description: Provide a clear and concise description of the challenge you are running into. 24 | validations: 25 | required: true 26 | - type: input 27 | id: link 28 | attributes: 29 | label: Your Example Website or App 30 | description: | 31 | Which website or app were you using when the bug happened? 32 | Note: 33 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the Remix Auth npm package. 34 | - To create a shareable code example you can use Stackblitz (https://stackblitz.com/). Please no localhost URLs. 35 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 36 | placeholder: | 37 | e.g. https://stackblitz.com/edit/...... OR Github Repo 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: steps 42 | attributes: 43 | label: Steps to Reproduce the Bug or Issue 44 | description: Describe the steps we have to take to reproduce the behavior. 45 | placeholder: | 46 | 1. Go to '...' 47 | 2. Click on '....' 48 | 3. Scroll down to '....' 49 | 4. See error 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: expected 54 | attributes: 55 | label: Expected behavior 56 | description: Provide a clear and concise description of what you expected to happen. 57 | placeholder: | 58 | As a user, I expected ___ behavior but i am seeing ___ 59 | validations: 60 | required: true 61 | - type: textarea 62 | id: screenshots_or_videos 63 | attributes: 64 | label: Screenshots or Videos 65 | description: | 66 | If applicable, add screenshots or a video to help explain your problem. 67 | For more information on the supported file image/file types and the file size limits, please refer 68 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 69 | placeholder: | 70 | You can drag your video or image files inside of this editor ↓ 71 | - type: textarea 72 | id: platform 73 | attributes: 74 | label: Platform 75 | value: | 76 | - OS: [e.g. macOS, Windows, Linux] 77 | - Browser: [e.g. Chrome, Safari, Firefox] 78 | - Version: [e.g. 91.1] 79 | validations: 80 | required: true 81 | - type: textarea 82 | id: additional 83 | attributes: 84 | label: Additional context 85 | description: Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Feature Requests & Questions 4 | url: https://github.com/xstevenyung/remix-crash/discussions 5 | about: Please ask and answer questions here. 6 | - name: 💬 Remix Discord Channel 7 | url: https://rmx.as/discord 8 | about: Interact with other people using Remix 📀 9 | - name: 💬 New Updates (Twitter) 10 | url: https://twitter.com/remix_run 11 | about: Stay up to date with Remix news on twitter 12 | - name: 🍿 Remix YouTube Channel 13 | url: https://www.youtube.com/channel/UC_9cztXyAZCli9Cky6NWWwQ 14 | about: Are you a techlead or wanting to learn more about Remix in depth? Checkout the Remix YouTube Channel -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to npm 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | npm: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: "14.x" 15 | registry-url: "https://registry.npmjs.org" 16 | 17 | - name: Cache pnpm modules 18 | uses: actions/cache@v2 19 | with: 20 | path: ~/.pnpm-store 21 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 22 | restore-keys: | 23 | ${{ runner.os }}- 24 | 25 | - uses: pnpm/action-setup@v2.0.1 26 | with: 27 | version: 6.24.0 28 | 29 | - name: Only run install on root package 30 | run: pnpm install --filter remix-crash 31 | 32 | - run: pnpx remix setup 33 | 34 | - run: pnpm run build 35 | 36 | - run: npm publish 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.local 4 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ DEPRECATED due to a lack of time, Remix Crash is deprecated. 2 | 3 | # Remix Crash 4 | 5 | A root development `` for your Remix apps. 6 | 7 | ![Remix Crash](./assets/screenshot.png) 8 | 9 | ## Overview 10 | 11 | Remix Crash is a development overlay to simplify debugging during your development process. 12 | 13 | **Warning**: Remix Crash is still quite recent, use at your own risk. 14 | 15 | ## Getting Started 16 | 17 | ### Installation 18 | 19 | ```bash 20 | npm install remix-crash 21 | ``` 22 | 23 | ### Setup 24 | 25 | #### In `app/root.tsx` 26 | 27 | ```jsx 28 | export default function App() { 29 | return { 30 | /* Your app */ 31 | }; 32 | } 33 | 34 | // Add this line 35 | export { ErrorBoundary } from "remix-crash"; 36 | ``` 37 | 38 | #### In `app/routes/_remix-crash.jsx` 39 | 40 | ```jsx 41 | export { loader, action } from "remix-crash/server"; 42 | ``` 43 | 44 | #### All set 45 | 46 | You should be all set from here. 47 | 48 | ## Advanced 49 | 50 | ### Production Error Boundary 51 | 52 | While Remix Crash provides a simple Production Error Boundary with less information. You might want to customize that page. 53 | 54 | If you choose to do so, you will just need to replace the `` component in your `app/root.jsx`: 55 | 56 | ```jsx 57 | // app/root.jsx 58 | // 1. Import the ErrorBoundary 59 | import { DevErrorBoundary } from "remix-crash"; 60 | 61 | export default function App() { 62 | return { 63 | /* Your app */ 64 | }; 65 | } 66 | 67 | // 2. Define your custom error boundary while using Remix Crash for development environment 68 | export function ErrorBoundary({ error }) { 69 | if (process.env.NODE_ENV === "development") { 70 | return ; 71 | } 72 | 73 | // here goes your custom production Error Boundary 74 | return ( 75 |
76 |

Oops something very wrong happened...

77 |
78 | ); 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/excessivecoding/remix-crash/f62f486a0b146c7feac922ffa56c4619164bc79f/assets/screenshot.png -------------------------------------------------------------------------------- /examples/latest/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /examples/latest/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Development 6 | 7 | From your terminal: 8 | 9 | ```sh 10 | npm run dev 11 | ``` 12 | 13 | This starts your app in development mode, rebuilding assets on file changes. 14 | 15 | ## Deployment 16 | 17 | First, build your app for production: 18 | 19 | ```sh 20 | npm run build 21 | ``` 22 | 23 | Then run the app in production mode: 24 | 25 | ```sh 26 | npm start 27 | ``` 28 | 29 | Now you'll need to pick a host to deploy it to. 30 | 31 | ### DIY 32 | 33 | If you're familiar with deploying node applications, the built-in Remix app server is production-ready. 34 | 35 | Make sure to deploy the output of `remix build` 36 | 37 | - `build/` 38 | - `public/build/` 39 | 40 | ### Using a Template 41 | 42 | When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server. 43 | 44 | ```sh 45 | cd .. 46 | # create a new project, and pick a pre-configured host 47 | npx create-remix@latest 48 | cd my-new-remix-app 49 | # remove the new project's app (not the old one!) 50 | rm -rf app 51 | # copy your app over 52 | cp -R ../my-old-remix-app/app app 53 | ``` 54 | -------------------------------------------------------------------------------- /examples/latest/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /examples/latest/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | const markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /examples/latest/app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LiveReload, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | } from "remix"; 9 | import type { MetaFunction } from "remix"; 10 | 11 | export const meta: MetaFunction = () => { 12 | return { title: "New Remix App" }; 13 | }; 14 | 15 | export default function App() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {process.env.NODE_ENV === "development" && } 29 | 30 | 31 | ); 32 | } 33 | 34 | export { ErrorBoundary } from "remix-crash"; 35 | -------------------------------------------------------------------------------- /examples/latest/app/routes/_remix-crash.tsx: -------------------------------------------------------------------------------- 1 | export { loader, action } from "remix-crash/server"; 2 | -------------------------------------------------------------------------------- /examples/latest/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, useLoaderData } from "remix"; 2 | import { format } from "date-fns"; 3 | 4 | type LoaderData = { now: Date }; 5 | 6 | export const loader: LoaderFunction = () => { 7 | const data: LoaderData = { now: new Date() }; 8 | 9 | return json(data); 10 | }; 11 | 12 | export default function Index() { 13 | const { now } = useLoaderData(); 14 | 15 | return ( 16 |
17 |

Welcome to Remix ({format(now, "dd")})

18 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /examples/latest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "remix-app-template", 4 | "description": "", 5 | "license": "", 6 | "scripts": { 7 | "build": "remix build", 8 | "dev": "remix dev", 9 | "postinstall": "remix setup node", 10 | "start": "remix-serve build" 11 | }, 12 | "dependencies": { 13 | "@remix-run/react": "^1.1.3", 14 | "@remix-run/serve": "^1.1.3", 15 | "date-fns": "^2.28.0", 16 | "remix": "^1.1.3", 17 | "remix-crash": "workspace:^0.1.2" 18 | }, 19 | "devDependencies": { 20 | "@remix-run/dev": "^1.1.3", 21 | "@types/react": "^17.0.24", 22 | "@types/react-dom": "^17.0.9", 23 | "typescript": "^4.1.2" 24 | }, 25 | "engines": { 26 | "node": ">=14" 27 | }, 28 | "sideEffects": false 29 | } 30 | -------------------------------------------------------------------------------- /examples/latest/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/excessivecoding/remix-crash/f62f486a0b146c7feac922ffa56c4619164bc79f/examples/latest/public/favicon.ico -------------------------------------------------------------------------------- /examples/latest/remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev/config').AppConfig} 3 | */ 4 | module.exports = { 5 | appDirectory: "app", 6 | assetsBuildDirectory: "public/build", 7 | publicPath: "/build/", 8 | serverBuildDirectory: "build", 9 | devServerPort: 8002, 10 | ignoredRouteFiles: [".*"] 11 | }; 12 | -------------------------------------------------------------------------------- /examples/latest/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/latest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | 17 | // Remix takes care of building everything in `remix build`. 18 | "noEmit": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/v1.0.5/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /.vercel 5 | /.output 6 | 7 | /public/build 8 | /api/build -------------------------------------------------------------------------------- /examples/v1.0.5/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Remix! 2 | 3 | - [Remix Docs](https://remix.run/docs) 4 | 5 | ## Vercel Setup 6 | 7 | First you'll need the [Vercel CLI](https://vercel.com/docs/cli): 8 | 9 | ```sh 10 | npm i -g vercel 11 | ``` 12 | 13 | Before you can run the app in development, you need to link this project to a new Vercel project on your account. 14 | 15 | **It is important that you use a new project. If you try to link this project to an existing project (like a Next.js site) you will have problems.** 16 | 17 | ```sh 18 | $ vercel link 19 | ``` 20 | 21 | Follow the prompts, and when it's done you should be able to get started. 22 | 23 | ## Development 24 | 25 | You will be running two processes during development when using Vercel as your server. 26 | 27 | - Your Vercel server in one 28 | - The Remix development server in another 29 | 30 | ```sh 31 | # in one tab 32 | $ vercel dev 33 | 34 | # in another 35 | $ npm run dev 36 | ``` 37 | 38 | Open up [http://localhost:3000](http://localhost:3000) and you should be ready to go! 39 | 40 | If you'd rather run everything in a single tab, you can look at [concurrently](https://npm.im/concurrently) or similar tools to run both processes in one tab. 41 | 42 | ## Deploying 43 | 44 | ```sh 45 | $ npm run build 46 | # preview deployment 47 | $ vercel 48 | 49 | # production deployment 50 | $ vercel --prod 51 | ``` 52 | 53 | ### GitHub Automatic Deployments 54 | 55 | For some reason the GitHub integration doesn't deploy the public folder. We're working with Vercel to figure this out. 56 | 57 | For now, [you can set up a GitHub action with this config](https://gist.github.com/mcansh/91f8effda798b41bb373351fad217070) from our friend [@mcansh](https://github.com/mcansh). 58 | -------------------------------------------------------------------------------- /examples/v1.0.5/api/index.js: -------------------------------------------------------------------------------- 1 | const { createRequestHandler } = require("@remix-run/vercel"); 2 | 3 | module.exports = createRequestHandler({ 4 | build: require("./build") 5 | }); 6 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | let markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/root.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Link, 4 | Links, 5 | LiveReload, 6 | Meta, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | useCatch, 11 | useLocation, 12 | } from "remix"; 13 | import type { LinksFunction } from "remix"; 14 | 15 | import deleteMeRemixStyles from "~/styles/demos/remix.css"; 16 | import globalStylesUrl from "~/styles/global.css"; 17 | import darkStylesUrl from "~/styles/dark.css"; 18 | 19 | /** 20 | * The `links` export is a function that returns an array of objects that map to 21 | * the attributes for an HTML `` element. These will load `` tags on 22 | * every route in the app, but individual routes can include their own links 23 | * that are automatically unloaded when a user navigates away from the route. 24 | * 25 | * https://remix.run/api/app#links 26 | */ 27 | export let links: LinksFunction = () => { 28 | return [ 29 | { rel: "stylesheet", href: globalStylesUrl }, 30 | { 31 | rel: "stylesheet", 32 | href: darkStylesUrl, 33 | media: "(prefers-color-scheme: dark)", 34 | }, 35 | { rel: "stylesheet", href: deleteMeRemixStyles }, 36 | ]; 37 | }; 38 | 39 | /** 40 | * The root module's default export is a component that renders the current 41 | * route via the `` component. Think of this as the global layout 42 | * component for your app. 43 | */ 44 | export default function App() { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | function Document({ 55 | children, 56 | title, 57 | }: { 58 | children: React.ReactNode; 59 | title?: string; 60 | }) { 61 | return ( 62 | 63 | 64 | 65 | 66 | {title ? {title} : null} 67 | 68 | 69 | 70 | 71 | {children} 72 | 73 | 74 | 75 | {process.env.NODE_ENV === "development" && } 76 | 77 | 78 | ); 79 | } 80 | 81 | function Layout({ children }: React.PropsWithChildren<{}>) { 82 | return ( 83 |
84 |
85 |
86 | 87 | 88 | 89 | 102 |
103 |
104 |
105 |
{children}
106 |
107 |
108 |
109 |

© You!

110 |
111 |
112 |
113 | ); 114 | } 115 | 116 | export function CatchBoundary() { 117 | let caught = useCatch(); 118 | 119 | let message; 120 | switch (caught.status) { 121 | case 401: 122 | message = ( 123 |

124 | Oops! Looks like you tried to visit a page that you do not have access 125 | to. 126 |

127 | ); 128 | break; 129 | case 404: 130 | message = ( 131 |

Oops! Looks like you tried to visit a page that does not exist.

132 | ); 133 | break; 134 | 135 | default: 136 | throw new Error(caught.data || caught.statusText); 137 | } 138 | 139 | return ( 140 | 141 | 142 |

143 | {caught.status}: {caught.statusText} 144 |

145 | {message} 146 |
147 |
148 | ); 149 | } 150 | 151 | export { ErrorBoundary } from "remix-crash"; 152 | 153 | function RemixLogo(props: React.ComponentPropsWithoutRef<"svg">) { 154 | return ( 155 | 167 | Remix Logo 168 | 169 | 170 | 171 | 172 | 173 | 174 | ); 175 | } 176 | 177 | /** 178 | * Provides an alert for screen reader users when the route changes. 179 | */ 180 | const RouteChangeAnnouncement = React.memo(() => { 181 | let [hydrated, setHydrated] = React.useState(false); 182 | let [innerHtml, setInnerHtml] = React.useState(""); 183 | let location = useLocation(); 184 | 185 | React.useEffect(() => { 186 | setHydrated(true); 187 | }, []); 188 | 189 | let firstRenderRef = React.useRef(true); 190 | React.useEffect(() => { 191 | // Skip the first render because we don't want an announcement on the 192 | // initial page load. 193 | if (firstRenderRef.current) { 194 | firstRenderRef.current = false; 195 | return; 196 | } 197 | 198 | let pageTitle = location.pathname === "/" ? "Home page" : document.title; 199 | setInnerHtml(`Navigated to ${pageTitle}`); 200 | }, [location.pathname]); 201 | 202 | // Render nothing on the server. The live region provides no value unless 203 | // scripts are loaded and the browser takes over normal routing. 204 | if (!hydrated) { 205 | return null; 206 | } 207 | 208 | return ( 209 |
227 | {innerHtml} 228 |
229 | ); 230 | }); 231 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/_remix-crash.tsx: -------------------------------------------------------------------------------- 1 | export { loader, action } from "remix-crash"; 2 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/about.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "remix"; 2 | import type { MetaFunction, LinksFunction } from "remix"; 3 | 4 | import stylesUrl from "~/styles/demos/about.css"; 5 | 6 | export let meta: MetaFunction = () => { 7 | return { 8 | title: "About Remix" 9 | }; 10 | }; 11 | 12 | export let links: LinksFunction = () => { 13 | return [{ rel: "stylesheet", href: stylesUrl }]; 14 | }; 15 | 16 | export default function Index() { 17 | return ( 18 |
19 |
20 |

About Us

21 |

22 | Ok, so this page isn't really about us, but we did want to 23 | show you a few more things Remix can do. 24 |

25 |

26 | Did you notice that things look a little different on this page? The 27 | CSS that we import in the route file and include in its{" "} 28 | links export is only included on this route and its 29 | children. 30 |

31 |

32 | Wait a sec...its children? To understand what we mean by 33 | this,{" "} 34 | 35 | read all about nested routes in the docs 36 | 37 | . 38 |

39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/about/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "remix"; 2 | 3 | export default function AboutIndex() { 4 | return ( 5 |
6 |

7 | You are looking at the index route for the /about URL 8 | segment, but there are nested routes as well! 9 |

10 |

11 | 12 | Check out one of them here. 13 | 14 |

15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/about/whoa.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "remix"; 2 | 3 | export default function AboutIndex() { 4 | return ( 5 |
6 |

7 | Whoa, this is a nested route! We render the /about layout 8 | route component, and its Outlet renders our route 9 | component. 🤯 10 |

11 |

12 | 13 | 14 | Go back to the /about index. 15 | 16 | 17 |

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/actions.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import type { ActionFunction } from "remix"; 3 | import { Form, json, useActionData, redirect } from "remix"; 4 | 5 | export function meta() { 6 | return { title: "Actions Demo" }; 7 | } 8 | 9 | // When your form sends a POST, the action is called on the server. 10 | // - https://remix.run/api/conventions#action 11 | // - https://remix.run/guides/data-updates 12 | export let action: ActionFunction = async ({ request }) => { 13 | let formData = await request.formData(); 14 | let answer = formData.get("answer"); 15 | 16 | // Typical action workflows start with validating the form data that just came 17 | // over the network. Clientside validation is fine, but you definitely need it 18 | // server side. If there's a problem, return the the data and the component 19 | // can render it. 20 | if (typeof answer !== "string") { 21 | return json("Come on, at least try!", { status: 400 }); 22 | } 23 | 24 | if (answer !== "egg") { 25 | return json(`Sorry, ${answer} is not right.`, { status: 400 }); 26 | } 27 | 28 | // Finally, if the data is valid, you'll typically write to a database or send or 29 | // email or log the user in, etc. It's recommended to redirect after a 30 | // successful action, even if it's to the same place so that non-JavaScript workflows 31 | // from the browser doesn't repost the data if the user clicks back. 32 | return redirect("/demos/correct"); 33 | }; 34 | 35 | export default function ActionsDemo() { 36 | // https://remix.run/api/remix#useactiondata 37 | let actionMessage = useActionData(); 38 | let answerRef = useRef(null); 39 | 40 | // This form works without JavaScript, but when we have JavaScript we can make 41 | // the experience better by selecting the input on wrong answers! Go ahead, disable 42 | // JavaScript in your browser and see what happens. 43 | useEffect(() => { 44 | if (actionMessage && answerRef.current) { 45 | answerRef.current.select(); 46 | } 47 | }, [actionMessage]); 48 | 49 | return ( 50 |
51 |
52 |

Actions!

53 |

54 | This form submission will send a post request that we handle in our 55 | `action` export. Any route can export an action to handle data 56 | mutations. 57 |

58 |
59 |

Post an Action

60 |

61 | What is more useful when it is broken? 62 |

63 | 67 |
68 | 69 |
70 | {actionMessage ? ( 71 |

72 | {actionMessage} 73 |

74 | ) : null} 75 |
76 |
77 | 78 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/correct.tsx: -------------------------------------------------------------------------------- 1 | export default function NiceWork() { 2 | return

You got it right!

; 3 | } 4 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/params.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData, Outlet } from "remix"; 2 | 3 | export function meta() { 4 | return { title: "Boundaries Demo" }; 5 | } 6 | 7 | export default function Boundaries() { 8 | return ( 9 |
10 |
11 | 12 |
13 | 14 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/params/$id.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData } from "remix"; 2 | import type { LoaderFunction, MetaFunction } from "remix"; 3 | 4 | // The `$` in route filenames becomes a pattern that's parsed from the URL and 5 | // passed to your loaders so you can look up data. 6 | // - https://remix.run/api/conventions#loader-params 7 | export let loader: LoaderFunction = async ({ params }) => { 8 | // pretend like we're using params.id to look something up in the db 9 | 10 | if (params.id === "this-record-does-not-exist") { 11 | // If the record doesn't exist we can't render the route normally, so 12 | // instead we throw a 404 reponse to stop running code here and show the 13 | // user the catch boundary. 14 | throw new Response("Not Found", { status: 404 }); 15 | } 16 | 17 | // now pretend like the record exists but the user just isn't authorized to 18 | // see it. 19 | if (params.id === "shh-its-a-secret") { 20 | // Again, we can't render the component if the user isn't authorized. You 21 | // can even put data in the response that might help the user rectify the 22 | // issue! Like emailing the webmaster for access to the page. (Oh, right, 23 | // `json` is just a Response helper that makes it easier to send JSON 24 | // responses). 25 | throw json({ webmasterEmail: "hello@remix.run" }, { status: 401 }); 26 | } 27 | 28 | // Sometimes your code just blows up and you never anticipated it. Remix will 29 | // automatically catch it and send the UI to the error boundary. 30 | if (params.id === "kaboom") { 31 | lol(); 32 | } 33 | 34 | // but otherwise the record was found, user has access, so we can do whatever 35 | // else we needed to in the loader and return the data. (This is boring, we're 36 | // just gonna return the params.id). 37 | return { param: params.id }; 38 | }; 39 | 40 | export default function ParamDemo() { 41 | let data = useLoaderData(); 42 | return ( 43 |

44 | The param is {data.param} 45 |

46 | ); 47 | } 48 | 49 | // https://remix.run/api/conventions#catchboundary 50 | // https://remix.run/api/remix#usecatch 51 | // https://remix.run/api/guides/not-found 52 | export function CatchBoundary() { 53 | let caught = useCatch(); 54 | 55 | let message: React.ReactNode; 56 | switch (caught.status) { 57 | case 401: 58 | message = ( 59 |

60 | Looks like you tried to visit a page that you do not have access to. 61 | Maybe ask the webmaster ({caught.data.webmasterEmail}) for access. 62 |

63 | ); 64 | case 404: 65 | message = ( 66 |

Looks like you tried to visit a page that does not exist.

67 | ); 68 | default: 69 | message = ( 70 |

71 | There was a problem with your request! 72 |
73 | {caught.status} {caught.statusText} 74 |

75 | ); 76 | } 77 | 78 | return ( 79 | <> 80 |

Oops!

81 |

{message}

82 |

83 | (Isn't it cool that the user gets to stay in context and try a different 84 | link in the parts of the UI that didn't blow up?) 85 |

86 | 87 | ); 88 | } 89 | 90 | // https://remix.run/api/conventions#errorboundary 91 | // https://remix.run/api/guides/not-found 92 | export function ErrorBoundary({ error }: { error: Error }) { 93 | console.error(error); 94 | return ( 95 | <> 96 |

Error!

97 |

{error.message}

98 |

99 | (Isn't it cool that the user gets to stay in context and try a different 100 | link in the parts of the UI that didn't blow up?) 101 |

102 | 103 | ); 104 | } 105 | 106 | export let meta: MetaFunction = ({ data }) => { 107 | return { 108 | title: data ? `Param: ${data.param}` : "Oops...", 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/demos/params/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCatch, Link, json, useLoaderData, Outlet } from "remix"; 2 | import type { LoaderFunction } from "remix"; 3 | 4 | export default function Boundaries() { 5 | return ( 6 | <> 7 |

Params

8 |

9 | When you name a route segment with $ like{" "} 10 | routes/users/$userId.js, the $ segment will be parsed from 11 | the URL and sent to your loaders and actions by the same name. 12 |

13 |

Errors

14 |

15 | When a route throws and error in it's action, loader, or component, 16 | Remix automatically catches it, won't even try to render the component, 17 | but it will render the route's ErrorBoundary instead. If the route 18 | doesn't have one, it will bubble up to the routes above it until it hits 19 | the root. 20 |

21 |

So be as granular as you want with your error handling.

22 |

Not Found

23 |

24 | (and other{" "} 25 | 26 | client errors 27 | 28 | ) 29 |

30 |

31 | Loaders and Actions can throw a Response instead of an 32 | error and Remix will render the CatchBoundary instead of the component. 33 | This is great when loading data from a database isn't found. As soon as 34 | you know you can't render the component normally, throw a 404 response 35 | and send your app into the catch boundary. Just like error boundaries, 36 | catch boundaries bubble, too. 37 |

38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction, LoaderFunction } from "remix"; 2 | import { useLoaderData, json, Link } from "remix"; 3 | 4 | type IndexData = { 5 | resources: Array<{ name: string; url: string }>; 6 | demos: Array<{ name: string; to: string }>; 7 | }; 8 | 9 | // Loaders provide data to components and are only ever called on the server, so 10 | // you can connect to a database or run any server side code you want right next 11 | // to the component that renders it. 12 | // https://remix.run/api/conventions#loader 13 | export let loader: LoaderFunction = () => { 14 | user.test(); 15 | let data: IndexData = { 16 | resources: [ 17 | { 18 | name: "Remix Docs", 19 | url: "https://remix.run/docs", 20 | }, 21 | { 22 | name: "React Router Docs", 23 | url: "https://reactrouter.com/docs", 24 | }, 25 | { 26 | name: "Remix Discord", 27 | url: "https://discord.gg/VBePs6d", 28 | }, 29 | ], 30 | demos: [ 31 | { 32 | to: "demos/actions", 33 | name: "Actions", 34 | }, 35 | { 36 | to: "demos/about", 37 | name: "Nested Routes, CSS loading/unloading", 38 | }, 39 | { 40 | to: "demos/params", 41 | name: "URL Params and Error Boundaries", 42 | }, 43 | ], 44 | }; 45 | 46 | // https://remix.run/api/remix#json 47 | return json(data); 48 | }; 49 | 50 | // https://remix.run/api/conventions#meta 51 | export let meta: MetaFunction = () => { 52 | return { 53 | title: "Remix Starter", 54 | description: "Welcome to remix!", 55 | }; 56 | }; 57 | 58 | // https://remix.run/guides/routing#index-routes 59 | export default function Index() { 60 | let data = useLoaderData(); 61 | 62 | return ( 63 |
64 |
65 |

Welcome to Remix!

66 |

We're stoked that you're here. 🥳

67 |

68 | Feel free to take a look around the code to see how Remix does things, 69 | it might be a bit different than what you’re used to. When you're 70 | ready to dive deeper, we've got plenty of resources to get you 71 | up-and-running quickly. 72 |

73 |

74 | Check out all the demos in this starter, and then just delete the{" "} 75 | app/routes/demos and app/styles/demos{" "} 76 | folders when you're ready to turn this into your next project. 77 |

78 |
79 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/styles/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-foreground: hsl(0, 0%, 100%); 3 | --color-background: hsl(0, 0%, 7%); 4 | --color-links: hsl(213, 100%, 73%); 5 | --color-links-hover: hsl(213, 100%, 80%); 6 | --color-border: hsl(0, 0%, 25%); 7 | } 8 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/styles/demos/about.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Whoa whoa whoa, wait a sec...why are we overriding global CSS selectors? 3 | * Isn't that kind of scary? How do we know this won't have side effects? 4 | * 5 | * In Remix, CSS that is included in a route file will *only* show up on that 6 | * route (and for nested routes, its children). When the user navigates away 7 | * from that route the CSS files linked from those routes will be automatically 8 | * unloaded, making your styles much easier to predict and control. 9 | * 10 | * Read more about styling routes in the docs: 11 | * https://remix.run/guides/styling 12 | */ 13 | 14 | :root { 15 | --color-foreground: hsl(0, 0%, 7%); 16 | --color-background: hsl(56, 100%, 50%); 17 | --color-links: hsl(345, 56%, 39%); 18 | --color-links-hover: hsl(345, 51%, 49%); 19 | --color-border: rgb(184, 173, 20); 20 | --font-body: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 21 | Liberation Mono, Courier New, monospace; 22 | } 23 | 24 | .about__intro { 25 | max-width: 500px; 26 | } 27 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/styles/demos/remix.css: -------------------------------------------------------------------------------- 1 | /* 2 | * You probably want to just delete this file; it's just for the demo pages. 3 | */ 4 | .remix-app { 5 | display: flex; 6 | flex-direction: column; 7 | min-height: 100vh; 8 | min-height: calc(100vh - env(safe-area-inset-bottom)); 9 | } 10 | 11 | .remix-app > * { 12 | width: 100%; 13 | } 14 | 15 | .remix-app__header { 16 | padding-top: 1rem; 17 | padding-bottom: 1rem; 18 | border-bottom: 1px solid var(--color-border); 19 | } 20 | 21 | .remix-app__header-content { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | } 26 | 27 | .remix-app__header-home-link { 28 | width: 106px; 29 | height: 30px; 30 | color: var(--color-foreground); 31 | } 32 | 33 | .remix-app__header-nav ul { 34 | list-style: none; 35 | margin: 0; 36 | display: flex; 37 | align-items: center; 38 | gap: 1.5em; 39 | } 40 | 41 | .remix-app__header-nav li { 42 | font-weight: bold; 43 | } 44 | 45 | .remix-app__main { 46 | flex: 1 1 100%; 47 | } 48 | 49 | .remix-app__footer { 50 | padding-top: 1rem; 51 | padding-bottom: 1rem; 52 | border-top: 1px solid var(--color-border); 53 | } 54 | 55 | .remix-app__footer-content { 56 | display: flex; 57 | justify-content: center; 58 | align-items: center; 59 | } 60 | 61 | .remix__page { 62 | --gap: 1rem; 63 | --space: 2rem; 64 | display: grid; 65 | grid-auto-rows: min-content; 66 | gap: var(--gap); 67 | padding-top: var(--space); 68 | padding-bottom: var(--space); 69 | } 70 | 71 | @media print, screen and (min-width: 640px) { 72 | .remix__page { 73 | --gap: 2rem; 74 | grid-auto-rows: unset; 75 | grid-template-columns: repeat(2, 1fr); 76 | } 77 | } 78 | 79 | @media screen and (min-width: 1024px) { 80 | .remix__page { 81 | --gap: 4rem; 82 | } 83 | } 84 | 85 | .remix__page > main > :first-child { 86 | margin-top: 0; 87 | } 88 | 89 | .remix__page > main > :last-child { 90 | margin-bottom: 0; 91 | } 92 | 93 | .remix__page > aside { 94 | margin: 0; 95 | padding: 1.5ch 2ch; 96 | border: solid 1px var(--color-border); 97 | border-radius: 0.5rem; 98 | } 99 | 100 | .remix__page > aside > :first-child { 101 | margin-top: 0; 102 | } 103 | 104 | .remix__page > aside > :last-child { 105 | margin-bottom: 0; 106 | } 107 | 108 | .remix__form { 109 | display: flex; 110 | flex-direction: column; 111 | gap: 1rem; 112 | padding: 1rem; 113 | border: 1px solid var(--color-border); 114 | border-radius: 0.5rem; 115 | } 116 | 117 | .remix__form > * { 118 | margin-top: 0; 119 | margin-bottom: 0; 120 | } 121 | -------------------------------------------------------------------------------- /examples/v1.0.5/app/styles/global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * You can just delete everything here or keep whatever you like, it's just a 3 | * quick baseline! 4 | */ 5 | :root { 6 | --color-foreground: hsl(0, 0%, 7%); 7 | --color-background: hsl(0, 0%, 100%); 8 | --color-links: hsl(213, 100%, 52%); 9 | --color-links-hover: hsl(213, 100%, 43%); 10 | --color-border: hsl(0, 0%, 82%); 11 | --font-body: -apple-system, "Segoe UI", Helvetica Neue, Helvetica, Roboto, 12 | Arial, sans-serif, system-ui, "Apple Color Emoji", "Segoe UI Emoji"; 13 | } 14 | 15 | html { 16 | box-sizing: border-box; 17 | } 18 | 19 | *, 20 | *::before, 21 | *::after { 22 | box-sizing: inherit; 23 | } 24 | 25 | :-moz-focusring { 26 | outline: auto; 27 | } 28 | 29 | :focus { 30 | outline: var(--color-links) solid 2px; 31 | outline-offset: 2px; 32 | } 33 | 34 | html, 35 | body { 36 | padding: 0; 37 | margin: 0; 38 | background-color: var(--color-background); 39 | color: var(--color-foreground); 40 | } 41 | 42 | body { 43 | font-family: var(--font-body); 44 | line-height: 1.5; 45 | } 46 | 47 | a { 48 | color: var(--color-links); 49 | text-decoration: none; 50 | } 51 | 52 | a:hover { 53 | color: var(--color-links-hover); 54 | text-decoration: underline; 55 | } 56 | 57 | hr { 58 | display: block; 59 | height: 1px; 60 | border: 0; 61 | background-color: var(--color-border); 62 | margin-top: 2rem; 63 | margin-bottom: 2rem; 64 | } 65 | 66 | input:where([type="text"]), 67 | input:where([type="search"]) { 68 | display: block; 69 | border: 1px solid var(--color-border); 70 | width: 100%; 71 | font: inherit; 72 | line-height: 1; 73 | height: calc(1ch + 1.5em); 74 | padding-right: 0.5em; 75 | padding-left: 0.5em; 76 | background-color: hsl(0 0% 100% / 20%); 77 | color: var(--color-foreground); 78 | } 79 | 80 | .sr-only { 81 | position: absolute; 82 | width: 1px; 83 | height: 1px; 84 | padding: 0; 85 | margin: -1px; 86 | overflow: hidden; 87 | clip: rect(0, 0, 0, 0); 88 | white-space: nowrap; 89 | border-width: 0; 90 | } 91 | 92 | .container { 93 | --gutter: 16px; 94 | width: 1024px; 95 | max-width: calc(100% - var(--gutter) * 2); 96 | margin-right: auto; 97 | margin-left: auto; 98 | } 99 | -------------------------------------------------------------------------------- /examples/v1.0.5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "remix-app-template", 4 | "description": "", 5 | "license": "", 6 | "scripts": { 7 | "build": "remix build", 8 | "dev": "remix dev", 9 | "postinstall": "remix setup node" 10 | }, 11 | "dependencies": { 12 | "@remix-run/react": "^1.0.5", 13 | "@remix-run/vercel": "^1.0.5", 14 | "react": "^17.0.2", 15 | "react-dom": "^17.0.2", 16 | "remix": "^1.0.5", 17 | "remix-crash": "workspace:^0.1.1" 18 | }, 19 | "devDependencies": { 20 | "@remix-run/dev": "^1.0.5", 21 | "@types/react": "^17.0.24", 22 | "@types/react-dom": "^17.0.9", 23 | "typescript": "^4.1.2" 24 | }, 25 | "engines": { 26 | "node": ">=14" 27 | }, 28 | "sideEffects": false 29 | } -------------------------------------------------------------------------------- /examples/v1.0.5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/excessivecoding/remix-crash/f62f486a0b146c7feac922ffa56c4619164bc79f/examples/v1.0.5/public/favicon.ico -------------------------------------------------------------------------------- /examples/v1.0.5/remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev/config').AppConfig} 3 | */ 4 | module.exports = { 5 | appDirectory: "app", 6 | browserBuildDirectory: "public/build", 7 | publicPath: "/build/", 8 | serverBuildDirectory: "api/build" 9 | }; 10 | -------------------------------------------------------------------------------- /examples/v1.0.5/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /examples/v1.0.5/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "esModuleInterop": true, 6 | "jsx": "react-jsx", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "target": "ES2019", 10 | "strict": true, 11 | "paths": { 12 | "~/*": ["./app/*"] 13 | }, 14 | 15 | // Remix takes care of building everything in `remix build`. 16 | "noEmit": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/v1.0.5/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | "ENABLE_FILE_SYSTEM_API": "1", 5 | "VERCEL_BUILD_CLI_PACKAGE": "vercel@canary" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-crash", 3 | "version": "0.1.3", 4 | "license": "MIT", 5 | "files": [ 6 | "dist" 7 | ], 8 | "exports": { 9 | ".": { 10 | "import": "./dist/client/remix-crash.es.js", 11 | "require": "./dist/client/remix-crash.umd.js", 12 | "types": "./dist/client/remix-crash.d.ts" 13 | }, 14 | "./server": { 15 | "import": "./dist/server/remix-crash.es.js", 16 | "require": "./dist/server/remix-crash.umd.js", 17 | "types": "./dist/server/remix-crash.d.ts" 18 | } 19 | }, 20 | "scripts": { 21 | "dev": "vite build --watch", 22 | "build": "tsc && vite build && rollup -c" 23 | }, 24 | "devDependencies": { 25 | "@remix-run/dev": "^1.0.5", 26 | "@remix-run/node": "^1.1.3", 27 | "@remix-run/react": "^1.0.5", 28 | "@rollup/plugin-typescript": "^8.3.0", 29 | "@types/babel__core": "^7.1.18", 30 | "@types/node": "^17.0.14", 31 | "@types/node-fetch": "^2.6.1", 32 | "@types/react": "^17.0.33", 33 | "@types/react-dom": "^17.0.10", 34 | "@vitejs/plugin-react": "^1.0.7", 35 | "autoprefixer": "^10.4.2", 36 | "concurrently": "^7.0.0", 37 | "postcss": "^8.4.6", 38 | "react": ">=16.8", 39 | "react-dom": ">=16.8", 40 | "remix": ">=1.0.5", 41 | "rollup": "^2.67.2", 42 | "tailwindcss": "^3.0.18", 43 | "typescript": "^4.4.4", 44 | "vite": "^2.7.2" 45 | }, 46 | "peerDependencies": { 47 | "react": ">=16.8", 48 | "react-dom": ">=16.8", 49 | "remix": ">=1.0.5" 50 | }, 51 | "dependencies": { 52 | "axios": "^0.26.0", 53 | "highlight.js": "^11.4.0", 54 | "node-fetch": "^3.2.0", 55 | "source-map": "^0.7.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - ./ 3 | - examples/** 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | 3 | export default { 4 | input: "src/server/index.ts", 5 | plugins: [typescript()], 6 | external: [ 7 | "react", 8 | "react-dom", 9 | "remix", 10 | "source-map", 11 | "fs/promises", 12 | "axios", 13 | ], 14 | output: [ 15 | { 16 | file: "dist/server/remix-crash.umd.js", 17 | name: "RemixCrash", 18 | format: "umd", 19 | globals: { 20 | remix: "Remix", 21 | "source-map": "SourceMap", 22 | "fs/promises": "FSPromises", 23 | axios: "Axios", 24 | }, 25 | }, 26 | { 27 | file: "dist/server/remix-crash.es.js", 28 | format: "esm", 29 | globals: { 30 | remix: "Remix", 31 | "source-map": "SourceMap", 32 | "fs/promises": "FSPromises", 33 | axios: "Axios", 34 | }, 35 | }, 36 | ], 37 | }; 38 | -------------------------------------------------------------------------------- /src/client/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | FunctionComponent, 4 | useContext, 5 | useEffect, 6 | useLayoutEffect, 7 | useMemo, 8 | useState, 9 | } from "react"; 10 | import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix"; 11 | import codeStyles from "highlight.js/styles/github-dark.css"; 12 | import styles from "./index.css"; 13 | import hljs from "highlight.js"; 14 | 15 | export type Stacktrace = Array; 16 | 17 | export type SanitizedStacktrace = Array<{ 18 | methodName: string; 19 | file: string; 20 | line: number; 21 | column: number; 22 | } | null>; 23 | 24 | export type ErrorState = { 25 | loading: boolean; 26 | stacktrace: Stacktrace; 27 | sanitizedStacktrace: SanitizedStacktrace; 28 | convertedStacktrace: Array<{ 29 | file: string; 30 | sourceContent: string; 31 | line: number; 32 | }>; 33 | selectedIndex: number | null; 34 | setSelectedIndex: (value: number | null) => void; 35 | }; 36 | 37 | export const defaultErrorState: ErrorState = { 38 | stacktrace: [], 39 | sanitizedStacktrace: [], 40 | loading: true, 41 | convertedStacktrace: [], 42 | setSelectedIndex: () => {}, 43 | selectedIndex: null, 44 | }; 45 | 46 | export const ErrorContext = createContext(defaultErrorState); 47 | 48 | export const ErrorContextProvider: FunctionComponent<{ 49 | stacktrace: Stacktrace; 50 | }> = ({ children, stacktrace }) => { 51 | const sanitizedStacktrace = useMemo(() => { 52 | return stacktrace.map((line) => { 53 | const sanitizedLine = line.replace("at", "").trim(); 54 | 55 | let result = null; 56 | 57 | result = new RegExp(/(.+) \((.+):(\d+):(\d+)\)/, "g").exec(sanitizedLine); 58 | 59 | if (result) { 60 | const [, methodName, file, line, column] = result; 61 | 62 | return { 63 | methodName, 64 | file, 65 | line, 66 | column, 67 | }; 68 | } 69 | 70 | result = new RegExp(/^(\/.+):(\d+):(\d+)/, "g").exec(sanitizedLine); 71 | 72 | if (result) { 73 | const [, file, line, column] = result; 74 | 75 | return { 76 | file, 77 | line, 78 | column, 79 | }; 80 | } 81 | }); 82 | }, [stacktrace]); 83 | 84 | const [convertedStacktrace, setConvertedStacktrace] = useState([]); 85 | const [loading, setLoading] = useState(true); 86 | 87 | useEffect(() => { 88 | Promise.all( 89 | sanitizedStacktrace.map(async (sanitizedStacktraceLine) => { 90 | if (!sanitizedStacktraceLine) return null; 91 | 92 | // @ts-ignore 93 | const params = new URLSearchParams(sanitizedStacktraceLine); 94 | 95 | return fetch(`/_remix-crash?${params.toString()}`, { 96 | method: "POST", 97 | }).then((response) => { 98 | if (!response.ok) return null; 99 | return response.json(); 100 | }); 101 | }) 102 | ) 103 | .then((data) => { 104 | // @ts-ignore 105 | setConvertedStacktrace(data); 106 | }) 107 | .finally(() => { 108 | setLoading(false); 109 | }); 110 | }, [sanitizedStacktrace]); 111 | 112 | const [selectedIndex, setSelectedIndex] = useState(null); 113 | 114 | useEffect(() => { 115 | setSelectedIndex(stacktrace.length ? 0 : null); 116 | }, [stacktrace]); 117 | 118 | return ( 119 | 130 | {children} 131 | 132 | ); 133 | }; 134 | 135 | export const useError = () => { 136 | return useContext(ErrorContext); 137 | }; 138 | 139 | export function ErrorBoundary({ error }: { error: Error }) { 140 | console.error(error); 141 | 142 | if (process.env.NODE_ENV !== "development") { 143 | return ; 144 | } 145 | 146 | return ; 147 | } 148 | 149 | export function ProdErrorBoundary({ error }: { error: Error }) { 150 | return ( 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 165 |
175 |

500

176 | 183 | Internal Server Error. 184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 | ); 192 | } 193 | 194 | export function DevErrorBoundary({ error }: { error: Error }) { 195 | const [firstLine, ...stacktrace] = useMemo(() => { 196 | if (!error.stack) return []; 197 | return error.stack.split("\n"); 198 | }, [error.stack]); 199 | 200 | const [type, message] = useMemo(() => { 201 | return firstLine?.split(": ") || ["", error.message]; 202 | }, [firstLine, error.message]); 203 | 204 | return ( 205 | 206 | 207 | {`💥 ${type}: ${message}`} 208 | 209 | 210 | 211 | 216 | 220 | 221 | 222 | 223 | 224 | 228 |
229 |
230 |
{type}
231 |
{message}
232 |
233 | 234 | 235 |
236 | 237 |
238 | 239 |
240 | 241 |
242 |
243 |
244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | ); 253 | } 254 | 255 | const StacktraceList = () => { 256 | const { stacktrace, loading } = useError(); 257 | 258 | if (loading) return null; 259 | 260 | return ( 261 |
    262 | {stacktrace.map((line, index) => { 263 | return ( 264 | 265 | {line} 266 | 267 | ); 268 | })} 269 |
270 | ); 271 | }; 272 | 273 | export const StackTraceLine: FunctionComponent<{ index: number }> = ({ 274 | index, 275 | children, 276 | }) => { 277 | const { convertedStacktrace, setSelectedIndex, loading, selectedIndex } = 278 | useError(); 279 | 280 | if (loading) return null; 281 | 282 | const line = convertedStacktrace[index]; 283 | 284 | return ( 285 |
  • 286 | 299 |
  • 300 | ); 301 | }; 302 | 303 | const CodeFrame = () => { 304 | const { convertedStacktrace, selectedIndex } = useError(); 305 | 306 | const convertedStacktraceLine = 307 | selectedIndex !== null ? convertedStacktrace[selectedIndex] : null; 308 | 309 | useLayoutEffect(() => { 310 | document.querySelector(".selected")?.scrollIntoView({ block: "center" }); 311 | }, [convertedStacktraceLine]); 312 | 313 | if (!convertedStacktraceLine) return null; 314 | 315 | return ( 316 |
    317 |       
    318 |         {hljs
    319 |           .highlight(convertedStacktraceLine.sourceContent, {
    320 |             language: "tsx",
    321 |           })
    322 |           .value.split("\n")
    323 |           .map((line, index) => {
    324 |             const selected = index + 1 === convertedStacktraceLine.line;
    325 | 
    326 |             return (
    327 |               
    335 | 342 | {index + 1} 343 | 344 |
    351 |
    352 | ); 353 | })} 354 |
    355 |
    356 | ); 357 | }; 358 | -------------------------------------------------------------------------------- /src/client/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ErrorBoundary"; 2 | -------------------------------------------------------------------------------- /src/server/endpoints.ts: -------------------------------------------------------------------------------- 1 | import { ActionFunction, LoaderFunction, json } from "remix"; 2 | import { SourceMapConsumer } from "source-map"; 3 | import { CORSResponse } from "./utils"; 4 | import { readFile } from "fs/promises"; 5 | import axios from "axios"; 6 | 7 | async function getFileContent(pathOrURL: string): Promise { 8 | const isRemote = new RegExp(/^http(s)?:\/\//).test(pathOrURL); 9 | 10 | if (isRemote) { 11 | return axios.get(pathOrURL).then((response) => response.data); 12 | } 13 | 14 | return readFile(pathOrURL, { encoding: "utf-8" }); 15 | } 16 | 17 | async function extractSourceMap(data: string, pathOrURL: string) { 18 | let sourceMapResult; 19 | 20 | sourceMapResult = new RegExp( 21 | /\n\/\/\# sourceMappingURL\=data\:application\/json\;base64\,(.*)/, 22 | "g" 23 | ).exec(data); 24 | 25 | if (sourceMapResult?.length) { 26 | const [_, base64SourceMap] = sourceMapResult; 27 | 28 | return JSON.parse(Buffer.from(base64SourceMap, "base64").toString("utf-8")); 29 | } 30 | 31 | sourceMapResult = new RegExp( 32 | /\/\/\# sourceMappingURL\=(\/(.*)\.map)/, 33 | "g" 34 | ).exec(data); 35 | 36 | if (sourceMapResult?.length) { 37 | const bundledFileURL = new URL(pathOrURL); 38 | const [_, sourceMapPath] = sourceMapResult; 39 | 40 | const rawSourceMap = await axios 41 | .get(sourceMapPath, { baseURL: bundledFileURL.origin }) 42 | .then((response) => response.data); 43 | 44 | return rawSourceMap; 45 | } 46 | 47 | return null; 48 | } 49 | 50 | export const loader: LoaderFunction = () => { 51 | if (process.env.NODE_ENV !== "development") { 52 | throw new Response(null, { status: 404 }); 53 | } 54 | 55 | return new CORSResponse(); 56 | }; 57 | 58 | export const action: ActionFunction = async ({ request }) => { 59 | if (process.env.NODE_ENV !== "development") { 60 | throw new Response(null, { status: 404 }); 61 | } 62 | 63 | const url = new URL(request.url); 64 | const root = process.cwd(); 65 | 66 | const bundledFile = url.searchParams.get("file"); 67 | const line = url.searchParams.get("line"); 68 | const column = url.searchParams.get("column"); 69 | 70 | if (!bundledFile || !line || !column) { 71 | throw new Response(null, { status: 422 }); 72 | } 73 | 74 | const data = await getFileContent(bundledFile); 75 | 76 | const rawSourceMap = await extractSourceMap(data, bundledFile); 77 | 78 | if (!rawSourceMap) { 79 | return json({ 80 | root, 81 | file: bundledFile.replace(root, ""), 82 | sourceContent: data, 83 | line: line ? parseInt(line) : null, 84 | column: column ? parseInt(column) : null, 85 | }); 86 | } 87 | 88 | const consumer = await new SourceMapConsumer(rawSourceMap); 89 | 90 | if (!line || !column) { 91 | throw new Error("Failed to load source map"); 92 | } 93 | 94 | const sourcePosition = consumer.originalPositionFor({ 95 | line: +line, 96 | column: +column ?? 0, 97 | }); 98 | 99 | if (!sourcePosition.source) { 100 | throw new Error("Failed to load source map"); 101 | } 102 | 103 | const sourceContent = 104 | consumer.sourceContentFor( 105 | sourcePosition.source, 106 | /* returnNullOnMissing */ true 107 | ) ?? null; 108 | 109 | const file = sourcePosition.source.replace("route-module:", ""); 110 | 111 | return json({ 112 | root, 113 | file: file.replace(root, ""), 114 | sourceContent, 115 | line: sourcePosition.line, 116 | column: sourcePosition.column, 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./endpoints"; 2 | -------------------------------------------------------------------------------- /src/server/utils.ts: -------------------------------------------------------------------------------- 1 | export class CORSHeaders extends Headers { 2 | constructor() { 3 | super(); 4 | 5 | this.append("Access-Control-Allow-Origin", "*"); 6 | 7 | this.append( 8 | "Access-Control-Allow-Methods", 9 | "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD" 10 | ); 11 | 12 | this.append( 13 | "Access-Control-Allow-Headers", 14 | "Content-Type, Accept, Authorization" 15 | ); 16 | } 17 | } 18 | 19 | export class CORSResponse extends Response { 20 | constructor() { 21 | super(null, { status: 204, headers: new CORSHeaders() }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], 3 | theme: { extend: {} }, 4 | plugins: [], 5 | }; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "react-jsx", 17 | "declaration": true, 18 | "outDir": "dist", 19 | "emitDeclarationOnly": true 20 | }, 21 | "include": ["src/client/index.ts", "src/server/index.ts", "src/vite-env.d.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig(({ mode }) => { 7 | return { 8 | plugins: [react()], 9 | define: 10 | mode === "production" 11 | ? { "process.env.NODE_ENV": "process.env.NODE_ENV" } 12 | : {}, 13 | build: { 14 | emptyOutDir: false, 15 | outDir: "dist/client", 16 | lib: { 17 | entry: path.resolve(__dirname, "src/client/index.ts"), 18 | name: "RemixCrash", 19 | fileName: (format) => `remix-crash.${format}.js`, 20 | }, 21 | rollupOptions: { 22 | // make sure to externalize deps that shouldn't be bundled 23 | // into your library 24 | external: [ 25 | "react", 26 | "react-dom", 27 | "remix", 28 | "source-map", 29 | "fs/promises", 30 | "axios", 31 | ], 32 | output: { 33 | // Provide global variables to use in the UMD build 34 | // for externalized deps 35 | globals: { 36 | react: "React", 37 | "react-dom": "ReactDOM", 38 | remix: "Remix", 39 | "source-map": "SourceMap", 40 | "fs/promises": "FSPromises", 41 | axios: "Axios", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }; 47 | }); 48 | --------------------------------------------------------------------------------