├── .gitignore ├── app ├── entry.client.tsx ├── routes │ ├── index.tsx │ └── _script.ts ├── islands │ └── counter.tsx ├── root.tsx ├── entry.server.tsx └── enhancements │ └── island.tsx ├── .eslintrc ├── remix.env.d.ts ├── public └── favicon.ico ├── remix.config.js ├── tsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | // We don't actually hydrate the app in this example 2 | export {}; 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] 3 | } 4 | -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacob-ebey/remix-preact-mpa-sprinkles/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@remix-run/dev').AppConfig} */ 2 | module.exports = { 3 | ignoredRouteFiles: ["**/.*"], 4 | // appDirectory: "app", 5 | // assetsBuildDirectory: "public/build", 6 | // serverBuildPath: "build/index.js", 7 | // publicPath: "/build/", 8 | }; 9 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import Island from "~/enhancements/island"; 2 | import CounterIsland from "~/islands/counter"; 3 | 4 | export default function Index() { 5 | return ( 6 |
7 |

Welcome to Remix

8 |

Counter Island

9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/islands/counter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | 3 | export default function CounterIsland({ 4 | initialCount, 5 | }: { 6 | initialCount?: number; 7 | }) { 8 | const [count, setCount] = useState(initialCount || 0); 9 | 10 | return ( 11 |

12 | 13 | {count} 14 | 15 |

16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import type { MetaFunction } from "@remix-run/node"; 2 | import { Links, LiveReload, Meta, Outlet } from "@remix-run/react"; 3 | 4 | export const meta: MetaFunction = () => ({ 5 | charset: "utf-8", 6 | title: "New Remix App", 7 | viewport: "width=device-width,initial-scale=1", 8 | }); 9 | 10 | export default function App() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /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 | "jsxImportSource": "preact", 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "target": "ES2019", 12 | "strict": true, 13 | "allowJs": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "~/*": ["./app/*"] 18 | }, 19 | 20 | // Remix takes care of building everything in `remix build`. 21 | "noEmit": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import type { EntryContext } from "@remix-run/node"; 2 | import { Response } from "@remix-run/node"; 3 | import { RemixServer } from "@remix-run/react"; 4 | import renderToString from "preact-render-to-string"; 5 | 6 | export default function handleRequest( 7 | request: Request, 8 | responseStatusCode: number, 9 | responseHeaders: Headers, 10 | remixContext: EntryContext 11 | ) { 12 | const html = renderToString( 13 | 14 | ); 15 | const headers = new Headers(responseHeaders); 16 | headers.set("content-type", "text/html"); 17 | 18 | return new Response("" + html, { 19 | status: responseStatusCode, 20 | headers, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "sideEffects": false, 4 | "scripts": { 5 | "build": "remix build", 6 | "dev": "remix dev", 7 | "start": "remix-serve build" 8 | }, 9 | "resolutions": { 10 | "react": "npm:@preact/compat", 11 | "react-dom": "npm:@preact/compat" 12 | }, 13 | "dependencies": { 14 | "@remix-run/node": "^1.7.2", 15 | "@remix-run/react": "^1.7.2", 16 | "@remix-run/serve": "^1.7.2", 17 | "esbuild": "^0.15.9", 18 | "preact": "^10.11.0", 19 | "preact-render-to-string": "^5.2.4", 20 | "react": "npm:@preact/compat", 21 | "react-dom": "npm:@preact/compat" 22 | }, 23 | "devDependencies": { 24 | "@remix-run/dev": "^1.7.2", 25 | "@remix-run/eslint-config": "^1.7.2", 26 | "eslint": "^8.23.1", 27 | "typescript": "^4.7.4" 28 | }, 29 | "engines": { 30 | "node": ">=14" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /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/enhancements/island.tsx: -------------------------------------------------------------------------------- 1 | import { type ComponentChildren, toChildArray } from "preact"; 2 | import { useId } from "preact/hooks"; 3 | import { dependencies } from "../../package.json"; 4 | 5 | const sha = process.env.RAILWAY_GIT_COMMIT_SHA; 6 | const preactVersion = dependencies.preact.replace(/^[\^~]/, ""); 7 | 8 | export default function Island({ 9 | source, 10 | children, 11 | }: { 12 | source: string; 13 | children: ComponentChildren; 14 | }) { 15 | let id = useId(); 16 | let childArray = toChildArray(children); 17 | let childProps: unknown; 18 | 19 | if (childArray.length !== 1) { 20 | throw new Error( 21 | `Island expects exactly one child, but received ${childArray.length}` 22 | ); 23 | } 24 | let child = childArray[0]; 25 | if (typeof child === "object" && child !== null) { 26 | let { children, ...props } = child.props; 27 | childProps = props; 28 | } 29 | 30 | let serialziedProps = childProps ? JSON.stringify(childProps) : "(void 0)"; 31 | 32 | let islandScriptParams = new URLSearchParams({ source }); 33 | sha && islandScriptParams.set("sha", sha); 34 | 35 | let scriptContent = `import{h,hydrate}from"https://esm.sh/preact@${preactVersion}";`; 36 | scriptContent += `import Island from "/_script?${islandScriptParams}";`; 37 | scriptContent += `let e=document.getElementById(${JSON.stringify( 38 | id 39 | )}).previousElementSibling;`; 40 | scriptContent += `let a=(n)=>e.replaceChild(n,e);`; 41 | scriptContent += `let p=${serialziedProps};`; 42 | scriptContent += `hydrate(h(Island, p), {`; 43 | scriptContent += `childNodes:[e],`; 44 | scriptContent += `firstChild:e,`; 45 | scriptContent += `insertBefore:a,`; 46 | scriptContent += `appendChild:a`; 47 | scriptContent += `});`; 48 | 49 | return ( 50 | <> 51 | {child} 52 |