├── .env.example ├── .gitignore ├── README.md ├── app ├── entry.client.tsx ├── entry.server.tsx ├── entry.worker.ts ├── root.tsx └── routes │ ├── about.tsx │ ├── index.tsx │ └── resources │ └── manifest[.]json.ts ├── package-lock.json ├── package.json ├── public └── icons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── remix.config.js ├── remix.env.d.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | /public/entry.worker.js 7 | .env 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from "react-dom"; 2 | import { RemixBrowser } from "remix"; 3 | 4 | hydrate(, document); 5 | 6 | if ("serviceWorker" in navigator) { 7 | // Use the window load event to keep the page load performant 8 | window.addEventListener("load", () => { 9 | navigator.serviceWorker 10 | .register("/entry.worker.js", { type: "module" }) 11 | .then(() => navigator.serviceWorker.ready) 12 | .then(() => { 13 | if (navigator.serviceWorker.controller) { 14 | navigator.serviceWorker.controller.postMessage({ 15 | type: "SYNC_REMIX_MANIFEST", 16 | manifest: window.__remixManifest, 17 | }); 18 | } else { 19 | navigator.serviceWorker.addEventListener("controllerchange", () => { 20 | navigator.serviceWorker.controller?.postMessage({ 21 | type: "SYNC_REMIX_MANIFEST", 22 | manifest: window.__remixManifest, 23 | }); 24 | }); 25 | } 26 | }) 27 | .catch((error) => { 28 | console.error("Service worker registration failed", error); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/entry.worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { json } from "@remix-run/server-runtime"; 4 | 5 | export type {}; 6 | declare let self: ServiceWorkerGlobalScope; 7 | 8 | let STATIC_ASSETS = ["/build/", "/icons/"]; 9 | 10 | let ASSET_CACHE = "asset-cache"; 11 | let DATA_CACHE = "data-cache"; 12 | let DOCUMENT_CACHE = "document-cache"; 13 | 14 | function debug(...messages: any[]) { 15 | if (process.env.NODE_ENV === "development") { 16 | console.debug(...messages); 17 | } 18 | } 19 | 20 | async function handleInstall(event: ExtendableEvent) { 21 | debug("Service worker installed"); 22 | } 23 | 24 | async function handleActivate(event: ExtendableEvent) { 25 | debug("Service worker activated"); 26 | } 27 | 28 | async function handleMessage(event: ExtendableMessageEvent) { 29 | let cachePromises: Map> = new Map(); 30 | 31 | if (event.data.type === "REMIX_NAVIGATION") { 32 | let { isMount, location, matches, manifest } = event.data; 33 | let documentUrl = location.pathname + location.search + location.hash; 34 | 35 | let [dataCache, documentCache, existingDocument] = await Promise.all([ 36 | caches.open(DATA_CACHE), 37 | caches.open(DOCUMENT_CACHE), 38 | caches.match(documentUrl), 39 | ]); 40 | 41 | if (!existingDocument || !isMount) { 42 | debug("Caching document for", documentUrl); 43 | cachePromises.set( 44 | documentUrl, 45 | documentCache.add(documentUrl).catch((error) => { 46 | debug(`Failed to cache document for ${documentUrl}:`, error); 47 | }) 48 | ); 49 | } 50 | 51 | if (isMount) { 52 | for (let match of matches) { 53 | if (manifest.routes[match.id].hasLoader) { 54 | let params = new URLSearchParams(location.search); 55 | params.set("_data", match.id); 56 | let search = params.toString(); 57 | search = search ? `?${search}` : ""; 58 | let url = location.pathname + search + location.hash; 59 | if (!cachePromises.has(url)) { 60 | debug("Caching data for", url); 61 | cachePromises.set( 62 | url, 63 | dataCache.add(url).catch((error) => { 64 | debug(`Failed to cache data for ${url}:`, error); 65 | }) 66 | ); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | await Promise.all(cachePromises.values()); 74 | } 75 | 76 | async function handleFetch(event: FetchEvent): Promise { 77 | let url = new URL(event.request.url); 78 | 79 | if (isAssetRequest(event.request)) { 80 | let cached = await caches.match(event.request, { 81 | cacheName: ASSET_CACHE, 82 | ignoreVary: true, 83 | ignoreSearch: true, 84 | }); 85 | if (cached) { 86 | debug("Serving asset from cache", url.pathname); 87 | return cached; 88 | } 89 | 90 | debug("Serving asset from network", url.pathname); 91 | let response = await fetch(event.request); 92 | if (response.status === 200) { 93 | let cache = await caches.open(ASSET_CACHE); 94 | await cache.put(event.request, response.clone()); 95 | } 96 | return response; 97 | } 98 | 99 | if (isLoaderRequest(event.request)) { 100 | try { 101 | debug("Serving data from network", url.pathname + url.search); 102 | let response = await fetch(event.request.clone()); 103 | let cache = await caches.open(DATA_CACHE); 104 | await cache.put(event.request, response.clone()); 105 | return response; 106 | } catch (error) { 107 | debug( 108 | "Serving data from network failed, falling back to cache", 109 | url.pathname + url.search 110 | ); 111 | let response = await caches.match(event.request); 112 | if (response) { 113 | response.headers.set("X-Remix-Worker", "yes"); 114 | return response; 115 | } 116 | 117 | return json( 118 | { message: "Network Error" }, 119 | { 120 | status: 500, 121 | headers: { "X-Remix-Catch": "yes", "X-Remix-Worker": "yes" }, 122 | } 123 | ); 124 | } 125 | } 126 | 127 | if (isDocumentGetRequest(event.request)) { 128 | try { 129 | debug("Serving document from network", url.pathname); 130 | let response = await fetch(event.request); 131 | let cache = await caches.open(DOCUMENT_CACHE); 132 | await cache.put(event.request, response.clone()); 133 | return response; 134 | } catch (error) { 135 | debug( 136 | "Serving document from network failed, falling back to cache", 137 | url.pathname 138 | ); 139 | let response = await caches.match(event.request); 140 | if (response) { 141 | return response; 142 | } 143 | throw error; 144 | } 145 | } 146 | 147 | return fetch(event.request.clone()); 148 | } 149 | 150 | function isMethod(request: Request, methods: string[]) { 151 | return methods.includes(request.method.toLowerCase()); 152 | } 153 | 154 | function isAssetRequest(request: Request) { 155 | return ( 156 | isMethod(request, ["get"]) && 157 | STATIC_ASSETS.some((publicPath) => request.url.startsWith(publicPath)) 158 | ); 159 | } 160 | 161 | function isLoaderRequest(request: Request) { 162 | let url = new URL(request.url); 163 | return isMethod(request, ["get"]) && url.searchParams.get("_data"); 164 | } 165 | 166 | function isDocumentGetRequest(request: Request) { 167 | return isMethod(request, ["get"]) && request.mode === "navigate"; 168 | } 169 | 170 | self.addEventListener("install", (event) => { 171 | event.waitUntil(handleInstall(event).then(() => self.skipWaiting())); 172 | }); 173 | 174 | self.addEventListener("activate", (event) => { 175 | event.waitUntil(handleActivate(event).then(() => self.clients.claim())); 176 | }); 177 | 178 | self.addEventListener("message", (event) => { 179 | event.waitUntil(handleMessage(event)); 180 | }); 181 | 182 | self.addEventListener("fetch", (event) => { 183 | event.respondWith( 184 | (async () => { 185 | let result = {} as 186 | | { error: unknown; response: undefined } 187 | | { error: undefined; response: Response }; 188 | try { 189 | result.response = await handleFetch(event); 190 | } catch (error) { 191 | result.error = error; 192 | } 193 | 194 | return appHandleFetch(event, result); 195 | })() 196 | ); 197 | }); 198 | 199 | async function appHandleFetch( 200 | event: FetchEvent, 201 | { 202 | error, 203 | response, 204 | }: 205 | | { error: unknown; response: undefined } 206 | | { error: undefined; response: Response } 207 | ): Promise { 208 | return response; 209 | } 210 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import type { ReactNode } from "react"; 3 | import { 4 | Link, 5 | Links, 6 | LiveReload, 7 | Meta, 8 | Outlet, 9 | Scripts, 10 | ScrollRestoration, 11 | useCatch, 12 | useLocation, 13 | useMatches, 14 | } from "remix"; 15 | import type { MetaFunction } from "remix"; 16 | 17 | import mainStylesHref from "awsm.css/dist/awsm.min.css"; 18 | import themeStylesHref from "awsm.css/dist/awsm_theme_mischka.min.css"; 19 | 20 | export const meta: MetaFunction = () => { 21 | return { 22 | title: "Remix PWA", 23 | description: "An example PWA built with Remix.", 24 | }; 25 | }; 26 | 27 | export default function RootRoute() { 28 | return ( 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export function CatchBoundary() { 36 | let { data, status } = useCatch(); 37 | 38 | let message = data?.message; 39 | if (!message) { 40 | switch (status) { 41 | case 404: 42 | message = "Page not found"; 43 | break; 44 | case 500: 45 | message = "Internal server error"; 46 | break; 47 | default: 48 | message = "Something went wrong"; 49 | } 50 | } 51 | 52 | return ( 53 | 54 |

{message}

55 |
56 | ); 57 | } 58 | 59 | export function ErrorBoundary({ error }: { error: Error }) { 60 | console.error("ERROR BOUNDARY", error); 61 | 62 | return ( 63 | 64 |

Something went wrong

65 |
66 | ); 67 | } 68 | 69 | let isMount = true; 70 | function Document({ children }: { children: ReactNode }) { 71 | let location = useLocation(); 72 | let matches = useMatches(); 73 | 74 | useEffect(() => { 75 | let mounted = isMount; 76 | isMount = false; 77 | if ("serviceWorker" in navigator) { 78 | if (navigator.serviceWorker.controller) { 79 | navigator.serviceWorker.controller?.postMessage({ 80 | type: "REMIX_NAVIGATION", 81 | isMount: mounted, 82 | location, 83 | matches, 84 | manifest: window.__remixManifest, 85 | }); 86 | } else { 87 | let listener = async () => { 88 | await navigator.serviceWorker.ready; 89 | navigator.serviceWorker.controller?.postMessage({ 90 | type: "REMIX_NAVIGATION", 91 | isMount: mounted, 92 | location, 93 | matches, 94 | manifest: window.__remixManifest, 95 | }); 96 | }; 97 | navigator.serviceWorker.addEventListener("controllerchange", listener); 98 | return () => { 99 | navigator.serviceWorker.removeEventListener( 100 | "controllerchange", 101 | listener 102 | ); 103 | }; 104 | } 105 | } 106 | }, [location]); 107 | 108 | return ( 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 124 | 129 | 134 | 139 | 144 | 149 | 154 | 159 | 164 | 170 | 176 | 182 | 188 | 189 | 190 | 191 |
192 |

Remix PWA

193 | 212 |
213 |
{children}
214 |
215 |

Remix Rocks

216 |
217 | 218 | 219 | {process.env.NODE_ENV === "development" && } 220 | 221 | 222 | ); 223 | } 224 | -------------------------------------------------------------------------------- /app/routes/about.tsx: -------------------------------------------------------------------------------- 1 | import { useLoaderData } from "remix"; 2 | import type { LoaderFunction } from "remix"; 3 | 4 | export let loader: LoaderFunction = () => { 5 | return { message: "About" }; 6 | }; 7 | 8 | export default function AboutRoute() { 9 | let { message } = useLoaderData(); 10 | 11 | return ( 12 |
13 |

{message}

14 |

15 | This example utilizes a hand crafted web worker that employs multiple 16 | caching strategies depending on the requests. 17 |

18 |
    19 |
  • 20 | Network First is used for document and data requests. 21 |
  • 22 |
  • 23 | Cache First is used for assets. 24 |
  • 25 |
26 | 27 |

Viewing the behavior

28 |

29 | I'm assuming you're using chrome and have used dev-tools before or are 30 | capable of googling: 31 |

32 |
    33 |
  1. Open dev-tools
  2. 34 |
  3. 35 | Open the Application tab, select Storage and click{" "} 36 | Clear site data 37 |
  4. 38 |
  5. Reload the page
  6. 39 |
  7. 40 | Click on the Home link above. This will do a client side 41 | navigation to the home page 42 |
  8. 43 |
  9. 44 | Open the Network tab and set throttling to Offline 45 |
  10. 46 |
47 | 48 |

49 | If you've followed the steps above, even though you did not visit the 50 | home page via a document request, you can still reload the page and 51 | access it. 52 |

53 |

54 | Similarly, even though you didn't client side navigate to the about 55 | page, if you click on the About link from the home page you can 56 | still access the about page. 57 |

58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useLoaderData } from "remix"; 2 | import type { LoaderFunction } from "remix"; 3 | 4 | export let loader: LoaderFunction = () => { 5 | return { message: "Welcome!" }; 6 | }; 7 | 8 | export default function IndexRoute() { 9 | let { message } = useLoaderData(); 10 | 11 | return ( 12 |
13 |

{message}

14 |

15 | This is an example PWA built with{" "} 16 | 17 | Remix 18 | 19 | . 20 |

21 |

22 | Since Remix utlizes SSR + client side navigations you actually have two 23 | ways the data ends up in your components: 24 |

25 |
    26 |
  1. embeded in the initial HTML for document requests
  2. 27 |
  3. 28 | or fetch() from the network for client side navigations 29 |
  4. 30 |
31 |

32 | To account for this when you landed on a page through a document request 33 | the service worker will fetch() the equivilant data in the 34 | background to simulate that you landed on the page through a cient side 35 | navigation. 36 |

37 |

38 | Similarly, if you land on a page through a client side navigation the 39 | service worker will simulate a full page document navigation and cache 40 | that in the background so when you reload the page you can still access 41 | it. 42 |

43 |

44 | Head on over to the about page for instructions 45 | on how to view this behavior. 46 |

47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/routes/resources/manifest[.]json.ts: -------------------------------------------------------------------------------- 1 | import { json } from "remix"; 2 | import type { LoaderFunction } from "remix"; 3 | 4 | export let loader: LoaderFunction = () => { 5 | return json( 6 | { 7 | short_name: "Remix TODO", 8 | name: "Remix TODO", 9 | start_url: "/", 10 | display: "standalone", 11 | background_color: "#d3d7dd", 12 | theme_color: "#c34138", 13 | icons: [ 14 | { 15 | src: "/icons/android-icon-36x36.png", 16 | sizes: "36x36", 17 | type: "image/png", 18 | density: "0.75", 19 | }, 20 | { 21 | src: "/icons/android-icon-48x48.png", 22 | sizes: "48x48", 23 | type: "image/png", 24 | density: "1.0", 25 | }, 26 | { 27 | src: "/icons/android-icon-72x72.png", 28 | sizes: "72x72", 29 | type: "image/png", 30 | density: "1.5", 31 | }, 32 | { 33 | src: "/icons/android-icon-96x96.png", 34 | sizes: "96x96", 35 | type: "image/png", 36 | density: "2.0", 37 | }, 38 | { 39 | src: "/icons/android-icon-144x144.png", 40 | sizes: "144x144", 41 | type: "image/png", 42 | density: "3.0", 43 | }, 44 | { 45 | src: "/icons/android-icon-192x192.png", 46 | sizes: "192x192", 47 | type: "image/png", 48 | density: "4.0", 49 | }, 50 | ], 51 | }, 52 | { 53 | headers: { 54 | "Cache-Control": "public, max-age=600", 55 | }, 56 | } 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "remix-pwa", 4 | "description": "", 5 | "license": "", 6 | "scripts": { 7 | "typecheck": "tsc", 8 | "build": "run-p build:*", 9 | "build:remix": "cross-env NODE_ENV=production remix build", 10 | "build:worker": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --minify --bundle --format=esm --define:process.env.NODE_ENV='\"production\"'", 11 | "dev": "run-p dev:*", 12 | "dev:remix": "dotenv -- remix dev", 13 | "dev:worker": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\"development\"' --watch", 14 | "start": "cross-env NODE_ENV=production remix-serve build", 15 | "postinstall": "remix setup node" 16 | }, 17 | "dependencies": { 18 | "@remix-run/react": "^1.1.3", 19 | "@remix-run/serve": "^1.1.3", 20 | "awsm.css": "^3.0.7", 21 | "react": "^17.0.2", 22 | "react-dom": "^17.0.2", 23 | "remix": "^1.1.3" 24 | }, 25 | "devDependencies": { 26 | "@remix-run/dev": "^1.1.3", 27 | "@types/react": "^17.0.24", 28 | "@types/react-dom": "^17.0.9", 29 | "cross-env": "^7.0.3", 30 | "dotenv-cli": "^4.1.1", 31 | "npm-run-all": "^4.1.5", 32 | "typescript": "^4.1.2" 33 | }, 34 | "engines": { 35 | "node": ">=14" 36 | }, 37 | "sideEffects": false 38 | } 39 | -------------------------------------------------------------------------------- /public/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/apple-icon.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-pwa/8a12cf63f5569f553b1faf6bba41d1693b2b1b52/public/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "exclude": ["app/entry.worker.ts"], 4 | "compilerOptions": { 5 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 6 | "isolatedModules": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-jsx", 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "target": "ES2019", 12 | "strict": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "~/*": ["./app/*"] 16 | }, 17 | 18 | // Remix takes care of building everything in `remix build`. 19 | "noEmit": true 20 | } 21 | } 22 | --------------------------------------------------------------------------------