├── .github └── workflows │ ├── release-please.yml │ └── semantic-prs.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .release-please-manifest.json ├── README.md ├── demo ├── README.md ├── index.html ├── netlify.toml ├── package.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── index.css │ ├── routes │ │ ├── hello.data.ts │ │ ├── hello.tsx │ │ ├── index.tsx │ │ └── world │ │ │ ├── [id].data.ts │ │ │ └── [id].tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package.json ├── packages ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── cli.mjs │ ├── package.json │ ├── plugin.d.ts │ ├── src │ │ ├── cli.ts │ │ ├── convertPathToURLPattern.ts │ │ ├── core.ts │ │ ├── dev.ts │ │ ├── plugin.ts │ │ ├── prerender.ts │ │ ├── shared.ts │ │ └── types.ts │ └── tsconfig.json ├── create-impala │ ├── CHANGELOG.md │ ├── create-impala.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── ambient.d.ts │ │ └── cli.ts │ └── tsconfig.json ├── preact │ ├── CHANGELOG.md │ ├── README.md │ ├── client.d.ts │ ├── head.d.ts │ ├── package.json │ ├── src │ │ ├── client.tsx │ │ ├── entry-server.tsx │ │ ├── head-context.tsx │ │ ├── head.tsx │ │ └── index.ts │ └── tsconfig.json └── react │ ├── CHANGELOG.md │ ├── README.md │ ├── client.d.ts │ ├── head.d.ts │ ├── package.json │ ├── src │ ├── client.tsx │ ├── entry-server.tsx │ ├── head-context.tsx │ ├── head.tsx │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── release-please-config.json ├── scripts ├── compile-template.ts ├── package.json └── tsconfig.json └── templates ├── preact-js ├── .gitignore ├── README.md ├── index.html ├── jsconfig.json ├── jsconfig.node.json ├── package.json ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ └── impala.png │ ├── entry-client.jsx │ ├── entry-server.jsx │ └── routes │ │ ├── hello.data.js │ │ ├── hello.jsx │ │ ├── index.css │ │ ├── index.jsx │ │ └── world │ │ ├── [id].data.js │ │ └── [id].jsx └── vite.config.js ├── preact-ts ├── .gitignore ├── README.md ├── index.html ├── package.json ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── impala.png │ ├── entry-client.tsx │ ├── entry-server.tsx │ ├── routes │ │ ├── hello.data.ts │ │ ├── hello.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── world │ │ │ ├── [id].data.ts │ │ │ └── [id].tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── react-js ├── README.md ├── index.html ├── jsconfig.json ├── jsconfig.node.json ├── package.json ├── src │ ├── App.css │ ├── App.jsx │ ├── assets │ │ └── impala.png │ ├── entry-client.jsx │ ├── entry-server.jsx │ └── routes │ │ ├── hello.data.js │ │ ├── hello.jsx │ │ ├── index.css │ │ ├── index.jsx │ │ └── world │ │ ├── [id].data.js │ │ └── [id].jsx └── vite.config.js └── react-ts ├── README.md ├── index.html ├── package.json ├── src ├── App.css ├── App.tsx ├── assets │ └── impala.png ├── entry-client.tsx ├── entry-server.tsx ├── routes │ ├── hello.data.ts │ ├── hello.tsx │ ├── index.css │ ├── index.tsx │ └── world │ │ ├── [id].data.ts │ │ └── [id].tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | steps: 13 | - uses: navikt/github-app-token-generator@v1 14 | id: get-token 15 | with: 16 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 17 | app-id: ${{ secrets.APP_ID }} 18 | - uses: google-github-actions/release-please-action@v3 19 | id: release 20 | with: 21 | command: manifest 22 | token: ${{ steps.get-token.outputs.token }} 23 | - uses: actions/checkout@v3 24 | if: ${{ steps.release.outputs.releases_created }} 25 | - uses: pnpm/action-setup@v2 26 | with: 27 | version: latest 28 | - uses: actions/setup-node@v3 29 | with: 30 | cache: "pnpm" 31 | check-latest: true 32 | registry-url: "https://registry.npmjs.org" 33 | if: ${{ steps.release.outputs.releases_created }} 34 | - name: Install dependencies 35 | run: pnpm install 36 | if: ${{ steps.release.outputs.releases_created }} 37 | - run: pnpm --filter "@impalajs/*" publish 38 | if: ${{ steps.release.outputs.releases_created }} 39 | - name: Authenticate pnpm 40 | run: pnpm config set '//registry.npmjs.org/:_authToken' "${NPM_AUTH_TOKEN}" 41 | if: ${{ steps.release.outputs.releases_created }} 42 | env: 43 | NPM_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 44 | - run: pnpm --filter "@impalajs/*" publish 45 | if: ${{ steps.release.outputs.releases_created }} 46 | -------------------------------------------------------------------------------- /.github/workflows/semantic-prs.yml: -------------------------------------------------------------------------------- 1 | name: "Lint PR" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | main: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5 19 | with: 20 | types: | 21 | fix 22 | feat 23 | chore 24 | docs 25 | ci 26 | test 27 | revert 28 | id: lint_pr_title 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - uses: marocchino/sticky-pull-request-comment@v2 33 | # When the previous steps fails, the workflow would stop. By adding this 34 | # condition you can continue the execution with the populated error message. 35 | if: always() && (steps.lint_pr_title.outputs.error_message != null) 36 | with: 37 | header: pr-title-lint-error 38 | message: | 39 | Hey there and thank you for opening this pull request! 👋🏼 40 | 41 | We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. 42 | Without this title format, a release will not be triggered 43 | 44 | Details: 45 | 46 | ``` 47 | ${{ steps.lint_pr_title.outputs.error_message }} 48 | ``` 49 | 50 | # Delete a previous comment when the issue has been resolved 51 | - if: ${{ steps.lint_pr_title.outputs.error_message == null }} 52 | uses: marocchino/sticky-pull-request-comment@v2 53 | with: 54 | header: pr-title-lint-error 55 | delete: true 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Local Netlify folder 27 | .netlify 28 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic/impala/74a4ab04744e8a539a3a7ec1225753c36ac040df/.npmrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {"packages/core":"0.0.14","packages/react":"0.0.14","packages/create-impala":"0.0.8","packages/preact":"0.0.14"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | 7 |

Very simple React and Preact static site generator

8 |

npm create impala@latest

9 | 10 | Impala is a bare-bones static-site framework, powered by [Vite](https://github.com/vitejs/vite). It currently supports [React](https://react.dev) and [Preact](https://preactjs.com/). Features include: 11 | 12 | - SSG-only, MPA-only. It's iMPAla, not iSPAla. 13 | - File-based routing, with a syntax like [Astro](https://github.com/withastro/astro) and [Solid Start](https://github.com/solidjs/solid-start) 14 | - Static and dynamic routes 15 | - Astro and [Next.js](https://github.com/vercel/next.js/)-inspired data fetching in `getStaticPaths`, and `getRouteData` 16 | - Route-level code-splitting 17 | - Optionally JS-free 18 | 19 | ## Usage 20 | 21 | ## Routing 22 | 23 | Create pages in `src/routes` and they will be available as routes in your site. For example, `src/routes/about.tsx` will be available at `/about`. You can also create dynamic routes, like `src/routes/blog/[slug].tsx`, but you'll need to add a `[slug].data.ts` file with a `getStaticPaths` function to tell Impala what paths to generate and the data to use. 24 | 25 | You can also do catch-all routes, like `src/routes/blog/[...slug].tsx`, which also needs a `[...slug].data.ts` file with a `getStaticPaths` function. 26 | 27 | ## Data fetching 28 | 29 | For dynamic routes you should fetch data in `getStaticPaths`. For static routes you should fetch data in `getRouteData`. For example, if you have a route at `src/routes/blog/[slug].tsx`, you should create a `src/routes/blog/[slug].data.ts` file with a `getStaticPaths` function. This function should return an array of paths to generate, and the data to use for each path. 30 | 31 | See the demo site for more. 32 | 33 | ## FAQ 34 | 35 | ### Why did you build this? 36 | 37 | Mainly to learn, but also because there's no statically-rendered [create-react-app](https://github.com/facebook/create-react-app) equivalent. I often want a simple React site with static rendering but no SSR. Astro is awesome, but I want something that's more vanilla React. 38 | 39 | ### Does it support SSR 40 | 41 | Deliberately not. If you want SSR, use Astro. 42 | 43 | ### Does it support client-side navigation? 44 | 45 | Deliberately not. If you want client-side navigation, use one of the many other SPA frameworks. 46 | 47 | ## License 48 | 49 | Copyright © 2023 Matt Kane. Licenced under the MIT licence. 50 | 51 | Impala logo created by [Freepik - Flaticon](https://www.flaticon.com/free-icons/impala) 52 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist/static" 3 | 4 | [dev] 5 | command = "npm run dev" -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "impala-demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "impala dev", 8 | "build:server": "vite build --ssr", 9 | "build:client": "vite build", 10 | "build:prerender": "impala prerender", 11 | "build": "npm run build:client && npm run build:server && npm run build:prerender", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@impalajs/core": "workspace:*", 16 | "@impalajs/react": "workspace:*", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^18.15.7", 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@vitejs/plugin-react": "^3.1.0", 25 | "create-impala": "workspace:*", 26 | "typescript": "^4.9.3", 27 | "vite": "^6.0.3" 28 | } 29 | } -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { Head } from "@impalajs/react/head"; 4 | 5 | interface AppProps { 6 | title: string; 7 | } 8 | 9 | export const App: React.FC> = ({ 10 | children, 11 | title, 12 | }) => { 13 | return ( 14 | <> 15 | 16 | {title} 17 | 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /demo/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import { clientBootstrap, RouteModule } from "@impalajs/react/client"; 2 | 3 | const modules = import.meta.glob("./routes/*.{tsx,jsx}"); 4 | 5 | clientBootstrap(modules); 6 | -------------------------------------------------------------------------------- /demo/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import type { RouteModule, DataModule } from "@impalajs/react"; 2 | export { render } from "@impalajs/react"; 3 | export const routeModules = import.meta.glob( 4 | "./routes/**/*.{tsx,jsx}" 5 | ); 6 | export const dataModules = import.meta.glob( 7 | "./routes/**/*.data.{ts,js}" 8 | ); 9 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | @media (prefers-color-scheme: light) { 59 | :root { 60 | color: #213547; 61 | background-color: #ffffff; 62 | } 63 | a:hover { 64 | color: #747bff; 65 | } 66 | button { 67 | background-color: #f9f9f9; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /demo/src/routes/hello.data.ts: -------------------------------------------------------------------------------- 1 | export function getRouteData() { 2 | return { 3 | msg: "hello world", 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/routes/hello.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import type { StaticRouteProps } from "@impalajs/core"; 3 | 4 | export default function Hello({ 5 | path, 6 | routeData, 7 | }: StaticRouteProps) { 8 | return ( 9 | 10 |
11 | <> 12 | {routeData?.msg} {path}! 13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /demo/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { StaticRouteProps } from "@impalajs/core"; 2 | import { useState } from "react"; 3 | import { App } from "../App"; 4 | 5 | export default function Hello({ path }: StaticRouteProps) { 6 | const [count, setCount] = useState(0); 7 | 8 | return ( 9 | 10 |
Home {path}!
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /demo/src/routes/world/[id].data.ts: -------------------------------------------------------------------------------- 1 | export function getStaticPaths() { 2 | return { 3 | paths: [ 4 | { params: { id: "1" }, data: { title: "One", description: "Page one" } }, 5 | { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, 6 | { 7 | params: { id: "3" }, 8 | data: { title: "Three", description: "Page three" }, 9 | }, 10 | ], 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /demo/src/routes/world/[id].tsx: -------------------------------------------------------------------------------- 1 | import { DynamicRouteProps } from "@impalajs/core"; 2 | import { App } from "../../App"; 3 | import { Head } from "@impalajs/react/head"; 4 | 5 | export default function Hello({ 6 | path, 7 | params, 8 | data, 9 | }: DynamicRouteProps) { 10 | return ( 11 | 12 | 13 | 14 | 15 |
16 | Hello {path} {params.id}! 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts", 10 | "vite-plugin" 11 | ] 12 | } -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import impala from "@impalajs/core/plugin"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), impala()], 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "impala-workspace", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "pnpm run --filter @impalajs/* build" 8 | }, 9 | "packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab", 10 | "pnpm": { 11 | "overrides": { 12 | "@impalajs/core": "workspace:*", 13 | "@impalajs/react": "workspace:*", 14 | "@impalajs/preact": "workspace:*", 15 | "create-impala": "workspace:*" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.14](https://github.com/ascorbic/impala/compare/core-v0.0.13...core-v0.0.14) (2024-12-09) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * support Vite 6 ([#39](https://github.com/ascorbic/impala/issues/39)) ([2221687](https://github.com/ascorbic/impala/commit/2221687544d533570f86b25848cf86d80b34f08d)) 9 | 10 | ## [0.0.13](https://github.com/ascorbic/impala/compare/core-v0.0.12...core-v0.0.13) (2023-11-05) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * use baseUrl for dev routing ([#34](https://github.com/ascorbic/impala/issues/34)) ([8a1e96a](https://github.com/ascorbic/impala/commit/8a1e96a38077bbf93327fd978d35e532f25a90c9)) 16 | 17 | ## [0.0.12](https://github.com/ascorbic/impala/compare/core-v0.0.11...core-v0.0.12) (2023-09-30) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **core:** use originalUrl for dev routing ([#29](https://github.com/ascorbic/impala/issues/29)) ([4fdd115](https://github.com/ascorbic/impala/commit/4fdd115a1c8c564ee144ce7a16895b9c6dd92636)) 23 | 24 | ## [0.0.11](https://github.com/ascorbic/impala/compare/core-v0.0.10...core-v0.0.11) (2023-09-08) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * ignore query string when routing ([#27](https://github.com/ascorbic/impala/issues/27)) ([d9701ae](https://github.com/ascorbic/impala/commit/d9701ae17393644fe5d5dae489760497777b77c1)) 30 | 31 | ## [0.0.10](https://github.com/ascorbic/impala/compare/core-v0.0.9...core-v0.0.10) (2023-04-07) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * avoid "max call stack" error in asset traversal ([#24](https://github.com/ascorbic/impala/issues/24)) ([c547af6](https://github.com/ascorbic/impala/commit/c547af64f1a810a3a9b26a23fd6f951bddc29f67)) 37 | 38 | ## [0.0.9](https://github.com/ascorbic/impala/compare/core-v0.0.8...core-v0.0.9) (2023-04-06) 39 | 40 | 41 | ### Bug Fixes 42 | 43 | * move assets to context ([#22](https://github.com/ascorbic/impala/issues/22)) ([e467cd5](https://github.com/ascorbic/impala/commit/e467cd53e3eab89f56c3a694b8f31fa2a5478272)) 44 | 45 | ## [0.0.8](https://github.com/ascorbic/impala/compare/core-v0.0.7...core-v0.0.8) (2023-04-06) 46 | 47 | 48 | ### Features 49 | 50 | * pass assets to render functions ([#20](https://github.com/ascorbic/impala/issues/20)) ([f7792c2](https://github.com/ascorbic/impala/commit/f7792c2a426357b1d16993b878589e638ec68df8)) 51 | 52 | ## [0.0.7](https://github.com/ascorbic/impala/compare/core-v0.0.6...core-v0.0.7) (2023-04-05) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * don't override custom vite config ([#18](https://github.com/ascorbic/impala/issues/18)) ([5dc148a](https://github.com/ascorbic/impala/commit/5dc148aba2575e11fd83d2730d14d0c8ad9fb926)) 58 | 59 | ## [0.0.6](https://github.com/ascorbic/impala/compare/core-v0.0.5...core-v0.0.6) (2023-03-28) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * correctly add links to head in dev mode ([#12](https://github.com/ascorbic/impala/issues/12)) ([5540a3d](https://github.com/ascorbic/impala/commit/5540a3d54edb608bba33e4023650e3eef63e3493)) 65 | 66 | ## 0.0.5 (2023-03-28) 67 | 68 | 69 | ### Features 70 | 71 | * add dev server ([#2](https://github.com/ascorbic/impala/issues/2)) ([bab917a](https://github.com/ascorbic/impala/commit/bab917a28df70d9df691f7d1db61bf6e140b7acb)) 72 | * add plugin ([544d4fa](https://github.com/ascorbic/impala/commit/544d4fa27d8e5eac8eb2858c8900c8cc7ce44755)) 73 | * add support for JavaScript ([#8](https://github.com/ascorbic/impala/issues/8)) ([ea5d1fa](https://github.com/ascorbic/impala/commit/ea5d1fa59623ae70c3ead2b58d5076e5d6605c74)) 74 | * load or preload assets in the head ([c5be211](https://github.com/ascorbic/impala/commit/c5be211614a712893e4a9c356850623683bf964d)) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * fix cli wrapper ([80172b2](https://github.com/ascorbic/impala/commit/80172b2cdc146ae2b248b79f20eb4cd98ea89b40)) 80 | * pass props to pages properly ([6b18945](https://github.com/ascorbic/impala/commit/6b189453d821ad85fdf828f5d270c754fecb0b26)) 81 | * pass props to pages properly ([#3](https://github.com/ascorbic/impala/issues/3)) ([ee3bb82](https://github.com/ascorbic/impala/commit/ee3bb8279987dcdd0655ef02a53bad883ee3413a)) 82 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 | ![impala](https://user-images.githubusercontent.com/213306/227727009-a4dc391f-efb1-4489-ad73-c3d3a327704a.png) 6 | 7 |

8 | -------------------------------------------------------------------------------- /packages/core/cli.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const currentVersion = process.versions.node; 3 | const currentMajor = parseInt(currentVersion.split(".")[0], 10); 4 | const minimumMajorVersion = 16; 5 | 6 | if (currentMajor < minimumMajorVersion) { 7 | console.error(`Node.js v${currentVersion} is unsupported!`); 8 | console.error(`Please use Node.js v${minimumMajorVersion} or higher.`); 9 | process.exit(1); 10 | } 11 | 12 | import("./dist/cli.mjs").then(({ main }) => main()); 13 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@impalajs/core", 3 | "version": "0.0.14", 4 | "description": "", 5 | "bin": { 6 | "impala": "./cli.mjs" 7 | }, 8 | "scripts": { 9 | "build": "tsup src/core.ts src/cli.ts src/plugin.ts --format esm --dts --clean" 10 | }, 11 | "module": "./dist/core.mjs", 12 | "types": "./dist/core.d.ts", 13 | "exports": { 14 | ".": { 15 | "types": "./dist/core.d.ts", 16 | "import": "./dist/core.mjs" 17 | }, 18 | "./plugin": { 19 | "types": "./dist/plugin.d.ts", 20 | "import": "./dist/plugin.mjs" 21 | } 22 | }, 23 | "keywords": [], 24 | "author": "", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/express": "^4.17.17", 28 | "tsup": "^6.7.0", 29 | "vite": "^6.0.3" 30 | }, 31 | "peerDependencies": { 32 | "vite": ">=4" 33 | }, 34 | "dependencies": { 35 | "express": "^4.18.2", 36 | "path-to-regexp": "^6.2.1", 37 | "radix3": "^1.0.0" 38 | }, 39 | "engines": { 40 | "node": ">=18.0.0" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } -------------------------------------------------------------------------------- /packages/core/plugin.d.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./dist/plugin"; 2 | -------------------------------------------------------------------------------- /packages/core/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "node:path"; 2 | import { createServer } from "./dev"; 3 | import { prerender } from "./prerender"; 4 | 5 | export async function main() { 6 | const [_, __, command, args] = process.argv; 7 | 8 | if (!command) { 9 | console.error("Command not specified"); 10 | process.exit(1); 11 | } 12 | 13 | if (command === "prerender") { 14 | const root = args ? resolve(args) : process.cwd(); 15 | await prerender(root); 16 | } else if (command === "dev") { 17 | createServer(); 18 | } else { 19 | console.error("Command not supported. Supported commands: dev, prerender"); 20 | process.exit(1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/convertPathToURLPattern.ts: -------------------------------------------------------------------------------- 1 | export function convertPathToPattern(path: string): string { 2 | return path 3 | .replace(/(\/index)?\.[jt]sx?$/, "") 4 | .replace(/(?:\[(\.{3})?(\w+)\])/g, (_match, isCatchAll, paramName) => { 5 | if (isCatchAll) { 6 | return `:${paramName}*`; 7 | } else { 8 | return `:${paramName}`; 9 | } 10 | }); 11 | } 12 | 13 | type Segment = { 14 | content: string; 15 | isDynamic: boolean; 16 | isCatchAll: boolean; 17 | }; 18 | 19 | type Pattern = { 20 | segments: Array; 21 | pattern: string; 22 | }; 23 | 24 | function convertPatternToSegments(pattern: string): Array { 25 | return pattern.split("/").map((content) => { 26 | const isDynamic = content.startsWith(":"); 27 | const isCatchAll = isDynamic && content.endsWith("*"); 28 | return { 29 | content, 30 | isDynamic, 31 | isCatchAll, 32 | }; 33 | }); 34 | } 35 | 36 | function comparePatterns(a: Pattern, b: Pattern): number { 37 | const length = Math.max(a.segments.length, b.segments.length); 38 | for (let index = 0; index < length; index++) { 39 | const p1 = a.segments[index]; 40 | const p2 = b.segments[index]; 41 | 42 | if (p1 === undefined) { 43 | return 1; 44 | } 45 | if (p2 === undefined) { 46 | return -1; 47 | } 48 | if (p1.isCatchAll && !p2.isCatchAll) { 49 | return 1; 50 | } 51 | if (p2.isCatchAll && !p1.isCatchAll) { 52 | return -1; 53 | } 54 | if (p1.isDynamic && !p2.isDynamic) { 55 | return 1; 56 | } 57 | if (p2.isDynamic && !p1.isDynamic) { 58 | // a is more specific 59 | return -1; 60 | } 61 | } 62 | // if we get here, fall back to string comparison 63 | return a.pattern.localeCompare(b.pattern); 64 | } 65 | 66 | export function sortPatterns(patterns: Array): Array { 67 | return Array.from(new Set(patterns)) 68 | .map((pattern) => { 69 | return { 70 | segments: convertPatternToSegments(pattern), 71 | pattern, 72 | }; 73 | }) 74 | .sort(comparePatterns) 75 | .map((pattern) => pattern.pattern); 76 | } 77 | -------------------------------------------------------------------------------- /packages/core/src/core.ts: -------------------------------------------------------------------------------- 1 | import { ModuleImports, RouteModule } from "./types"; 2 | import { createRouter, RadixRouter } from "radix3"; 3 | export * from "./shared"; 4 | export * from "./types"; 5 | 6 | export function convertPathToPattern(input: string): string { 7 | return input 8 | .replace(/^\.\/routes/, "") 9 | .replace(/(\/index)?\.[jt]sx?$/, "") 10 | .replace(/(?:\[(\.{3})?(\w+)\])/g, (_match, isCatchAll, paramName) => { 11 | if (isCatchAll) { 12 | return `:${paramName}*`; 13 | } else { 14 | return `:${paramName}`; 15 | } 16 | }); 17 | } 18 | 19 | /** 20 | * Converts a module path to a radix3 route 21 | */ 22 | export function convertPathToRoute(input: string): string { 23 | return input 24 | .replace(/^\.\/routes/, "") 25 | .replace(/(\/index)?\.[jt]sx?$/, "") 26 | .replace(/(?:\[(\.{3})?(\w+)\])/g, (_match, isCatchAll, paramName) => { 27 | if (isCatchAll) { 28 | return `**:${paramName}`; 29 | } else { 30 | return `:${paramName}`; 31 | } 32 | }); 33 | } 34 | 35 | export function routerForModules( 36 | modules: ModuleImports 37 | ): RadixRouter<{ chunk: string }> { 38 | const router = createRouter<{ chunk: string }>(); 39 | for (const path in modules) { 40 | router.insert(convertPathToRoute(path), { chunk: path }); 41 | } 42 | return router; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/src/dev.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { createServer as createViteServer } from "vite"; 3 | import { routerForModules } from "./core"; 4 | import { ServerEntry } from "./types"; 5 | import { readFileSync } from "node:fs"; 6 | import { resolve } from "node:path"; 7 | import { renderLinkTagsForModuleNode } from "./shared"; 8 | 9 | function isDynamicRoute(route: string) { 10 | return route.includes("/["); 11 | } 12 | 13 | function stripExtension(path: string) { 14 | return path.replace(/\.[^/.]+$/, ""); 15 | } 16 | 17 | function shallowCompare( 18 | obj1?: Record, 19 | obj2?: Record 20 | ): boolean { 21 | // Check if both objects are null or undefined 22 | if (obj1 == obj2) { 23 | return true; 24 | } 25 | 26 | if (!obj1 || !obj2) { 27 | return false; 28 | } 29 | 30 | const keys1 = Object.keys(obj1); 31 | const keys2 = Object.keys(obj2); 32 | 33 | if (keys1.length !== keys2.length) { 34 | return false; 35 | } 36 | for (const key of keys1) { 37 | if (obj1[key] !== obj2[key]) { 38 | return false; 39 | } 40 | } 41 | 42 | return true; 43 | } 44 | 45 | /** 46 | * We can't use the vite dev server directly, because we need to do SSR. 47 | * Because of this, we create a custom server and use the vite server as middleware. 48 | */ 49 | export async function createServer() { 50 | console.log("Starting dev server!"); 51 | const app = express(); 52 | 53 | const vite = await createViteServer({ 54 | server: { middlewareMode: true }, 55 | appType: "custom", 56 | }); 57 | app.use(vite.middlewares); 58 | 59 | app.use("*", async (req, res, next) => { 60 | try { 61 | const template = await readFileSync( 62 | resolve(vite.config.root, "index.html"), 63 | "utf-8" 64 | ); 65 | 66 | const { render, routeModules, dataModules } = (await vite.ssrLoadModule( 67 | "/src/entry-server" 68 | )) as ServerEntry; 69 | 70 | const router = routerForModules(routeModules); 71 | 72 | const result = router.lookup(req.baseUrl); 73 | 74 | if (!result) { 75 | res.status(404).end("404"); 76 | return; 77 | } 78 | 79 | const mod = routeModules[result.chunk]; 80 | const baseRoute = stripExtension(result.chunk); 81 | // Try and find a data module for this route 82 | const dataMod = 83 | dataModules[`${baseRoute}.data.ts`] || 84 | dataModules[`${baseRoute}.data.js`]; 85 | 86 | const { getStaticPaths, getRouteData } = (await dataMod?.()) || {}; 87 | 88 | const routeData = await getRouteData?.(); 89 | 90 | let data: Awaited< 91 | ReturnType 92 | >["paths"][number]["data"]; 93 | 94 | if (isDynamicRoute(result.chunk)) { 95 | const { paths } = (await getStaticPaths?.()) || {}; 96 | const matched = paths?.find((p) => 97 | shallowCompare(p.params, result.params) 98 | ); 99 | data = matched?.data; 100 | console.log({ matched }); 101 | 102 | if (!matched) { 103 | console.log("No match for dynamic route", result.chunk, paths); 104 | res.status(404).end("404"); 105 | return; 106 | } 107 | } 108 | 109 | const context = { 110 | path: req.originalUrl, 111 | routeData, 112 | chunk: result.chunk, 113 | params: result.params, 114 | data, 115 | }; 116 | 117 | const { body, head } = await render(context, mod, []); 118 | const node = vite.moduleGraph.urlToModuleMap.get( 119 | result.chunk.replace(/^\.\//, "/src/") 120 | ); 121 | const links = node ? renderLinkTagsForModuleNode(node) : ""; 122 | const transformed = await vite.transformIndexHtml( 123 | req.originalUrl, 124 | template 125 | .replace("", head + links) 126 | .replace("", body) 127 | ); 128 | 129 | res.status(200).set({ "Content-Type": "text/html" }).end(transformed); 130 | } catch (e) { 131 | vite.ssrFixStacktrace(e as Error); 132 | next(e); 133 | } 134 | }); 135 | 136 | app.listen(5173); 137 | console.log("Listening on http://localhost:5173"); 138 | } 139 | -------------------------------------------------------------------------------- /packages/core/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, ResolvedConfig } from "vite"; 2 | 3 | export default function plugin(): Plugin { 4 | // const virtualModuleId = "virtual:route-manifest"; 5 | 6 | let config: ResolvedConfig; 7 | // const resolvedVirtualModuleId = "\0" + virtualModuleId; 8 | // console.log("resolvedVirtualModuleId", resolvedVirtualModuleId); 9 | return { 10 | name: "impala-plugin", 11 | 12 | config(config) { 13 | config.build ||= {}; 14 | config.build.manifest = true; 15 | 16 | if (config.build.ssr) { 17 | if (config.build.ssr === true) { 18 | config.build.ssr = "src/entry-server"; 19 | } 20 | config.build.outDir ||= "dist/server"; 21 | } else { 22 | config.build.outDir ||= "dist/static"; 23 | config.build.ssrManifest = true; 24 | config.build.rollupOptions ||= {}; 25 | config.build.rollupOptions.input ||= "index.html"; 26 | } 27 | }, 28 | 29 | configResolved(resolvedConfig) { 30 | config = resolvedConfig; 31 | }, 32 | // resolveId(id, source) { 33 | // if (id === virtualModuleId) { 34 | // return resolvedVirtualModuleId; 35 | // } 36 | // }, 37 | // load(id) { 38 | // // if (id.endsWith("tsx")) { 39 | // // console.log("id", this.getModuleInfo(id)); 40 | // // } 41 | // if (id === resolvedVirtualModuleId) { 42 | // return `export const msg = "from virtual module"`; 43 | // } 44 | // }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/prerender.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs, existsSync } from "node:fs"; 2 | import path from "node:path"; 3 | import { compile } from "path-to-regexp"; 4 | import { convertPathToPattern } from "./core"; 5 | import { 6 | findAssetsInManifest, 7 | renderAssetLinkTags, 8 | renderLinkTagsForManifestChunk, 9 | } from "./shared"; 10 | import { Context, RouteModuleFunction, ServerEntry } from "./types"; 11 | 12 | function isDynamicRoute(route: string) { 13 | return route.includes("/["); 14 | } 15 | 16 | function stripExtension(path: string) { 17 | return path.replace(/\.[^/.]+$/, ""); 18 | } 19 | 20 | export async function prerender(root: string) { 21 | const { render, routeModules, dataModules } = (await import( 22 | path.resolve(root, "./dist/server/entry-server.js") 23 | )) as ServerEntry; 24 | 25 | const template = await fs.readFile( 26 | path.resolve(root, "./dist/static/index.html"), 27 | "utf-8" 28 | ); 29 | const manifestPaths = [ 30 | "dist/static/manifest.json", 31 | "dist/static/.vite/manifest.json", 32 | ].map((p) => path.resolve(root, p)); 33 | 34 | const manifestPath = manifestPaths.find((p) => existsSync(p)); 35 | if (!manifestPath) { 36 | console.error( 37 | `No manifest found at ${manifestPath}. Did you build the app?` 38 | ); 39 | return; 40 | } 41 | const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8")); 42 | 43 | const assetMap = new Map(); 44 | 45 | async function prerenderRoute(context: Context, mod: RouteModuleFunction) { 46 | const { body, head } = await render(context, mod, []); 47 | 48 | // If the render function returns false for head, it means that the 49 | // body is the full document, so we don't need to wrap it in the 50 | // template. 51 | const appHtml = 52 | head === false 53 | ? body 54 | : template 55 | .replace( 56 | "", 57 | renderAssetLinkTags(context.assets ?? []) + head 58 | ) 59 | .replace("", body); 60 | 61 | const filePath = `dist/static${ 62 | context.path === "/" ? "/index" : context.path 63 | }.html`; 64 | 65 | const dir = path.dirname(path.resolve(root, filePath)); 66 | if (!existsSync(dir)) { 67 | await fs.mkdir(dir, { 68 | recursive: true, 69 | }); 70 | } 71 | if (!(await fs.stat(dir)).isDirectory()) { 72 | console.error(`Cannot create directory: ${dir}`); 73 | return; 74 | } 75 | 76 | await fs.writeFile(filePath, appHtml); 77 | console.log("pre-rendered:", filePath); 78 | } 79 | 80 | for (const route in routeModules) { 81 | if (Object.hasOwnProperty.call(routeModules, route)) { 82 | const mod = routeModules[route]; 83 | const baseRoute = stripExtension(route); 84 | const dataMod = 85 | dataModules[`${baseRoute}.data.ts`] || 86 | dataModules[`${baseRoute}.data.js`]; 87 | 88 | const { getStaticPaths, getRouteData } = (await dataMod?.()) || {}; 89 | 90 | const routeData = await getRouteData?.(); 91 | const routePattern = convertPathToPattern(route) || "/"; 92 | const assets = findAssetsInManifest( 93 | manifest, 94 | route.replace(/^\.\//, "src/"), 95 | assetMap 96 | ); 97 | if (isDynamicRoute(route)) { 98 | console.log(`pre-rendering dynamic route: ${route}`); 99 | if (!dataMod) { 100 | console.warn(`No data module found for dynamic route: ${route}`); 101 | console.warn( 102 | `You should export 'getStaticPaths' from ${baseRoute}.data.ts` 103 | ); 104 | continue; 105 | } 106 | 107 | if (!getStaticPaths) { 108 | console.warn( 109 | `No 'getStaticPaths' export found in ${baseRoute}.data.ts. No pages will be generated.` 110 | ); 111 | continue; 112 | } 113 | 114 | const toPath = compile(routePattern, { encode: encodeURIComponent }); 115 | 116 | const { paths } = await getStaticPaths(); 117 | 118 | await Promise.all( 119 | paths.map((pathInfo) => { 120 | const path = toPath(pathInfo.params); 121 | if (!path) { 122 | console.error(`Invalid path params for route: ${route}`); 123 | return; 124 | } 125 | 126 | return prerenderRoute( 127 | { 128 | path, 129 | routeData, 130 | chunk: route, 131 | params: pathInfo.params, 132 | data: pathInfo.data, 133 | assets, 134 | }, 135 | mod 136 | ); 137 | }) 138 | ); 139 | } else { 140 | await prerenderRoute( 141 | { path: routePattern, chunk: route, assets, routeData }, 142 | mod 143 | ); 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/core/src/shared.ts: -------------------------------------------------------------------------------- 1 | import { basename } from "node:path"; 2 | import { ModuleNode, Manifest } from "vite"; 3 | 4 | /** 5 | * Traverses the module graph and collects assets for a given chunk 6 | * 7 | * @param manifest Client manifest 8 | * @param id Chunk id 9 | * @param assetMap Cache of assets 10 | * @returns Array of asset URLs 11 | */ 12 | export const findAssetsInManifest = ( 13 | manifest: Manifest, 14 | id: string, 15 | assetMap: Map> = new Map() 16 | ): Array => { 17 | function traverse(id: string): Array { 18 | const cached = assetMap.get(id); 19 | if (cached) { 20 | return cached; 21 | } 22 | const chunk = manifest[id]; 23 | if (!chunk) { 24 | return []; 25 | } 26 | const assets = [ 27 | ...(chunk.assets || []), 28 | ...(chunk.css || []), 29 | ...(chunk.imports?.flatMap(traverse) || []), 30 | ]; 31 | const imports = chunk.imports?.flatMap(traverse) || []; 32 | const all = [...assets, ...imports].filter( 33 | Boolean as unknown as (a: string | undefined) => a is string 34 | ); 35 | all.push(chunk.file); 36 | assetMap.set(id, all); 37 | return Array.from(new Set(all)); 38 | } 39 | return traverse(id); 40 | }; 41 | 42 | export const findAssetsInModuleNode = (moduleNode: ModuleNode) => { 43 | const seen = new Set(); 44 | function traverse(node: ModuleNode): Array { 45 | if (seen.has(node.url)) { 46 | return []; 47 | } 48 | seen.add(node.url); 49 | 50 | const imports = [...node.importedModules].flatMap(traverse) || []; 51 | imports.push(node.url); 52 | return Array.from(new Set(imports)); 53 | } 54 | return traverse(moduleNode); 55 | }; 56 | 57 | /** 58 | * Traverses the module graph and generates link tags to either import or preload asets 59 | */ 60 | export function renderLinkTagsForManifestChunk( 61 | manifest: Manifest, 62 | id: string, 63 | cachedAssetMap?: Map> 64 | ): string { 65 | const assets = findAssetsInManifest(manifest, id, cachedAssetMap); 66 | return assets.map((asset) => renderLinkTag(`/${asset}`)).join(""); 67 | } 68 | 69 | /** 70 | * Finds all the imported modules for a given module and generates link tags to 71 | * either import or preload them. 72 | */ 73 | export const renderLinkTagsForModuleNode = (node: ModuleNode): string => { 74 | const assets = findAssetsInModuleNode(node); 75 | return assets.map(renderLinkTag).join(""); 76 | }; 77 | 78 | export function renderAssetLinkTags(assets: Array): string { 79 | return assets.map((asset) => renderLinkTag(`/${asset}`)).join(""); 80 | } 81 | 82 | interface LinkProps { 83 | rel: string; 84 | as?: string; 85 | type?: string; 86 | crossorigin?: string; 87 | } 88 | 89 | const linkPropsMap: Map = new Map([ 90 | ["js", { rel: "modulepreload", crossorigin: "true" }], 91 | ["jsx", { rel: "modulepreload", crossorigin: "true" }], 92 | ["ts", { rel: "modulepreload", crossorigin: "true" }], 93 | ["tsx", { rel: "modulepreload", crossorigin: "true" }], 94 | ["css", { rel: "stylesheet" }], 95 | [ 96 | "woff", 97 | { rel: "preload", as: "font", type: "font/woff", crossorigin: "true" }, 98 | ], 99 | [ 100 | "woff2", 101 | { rel: "preload", as: "font", type: "font/woff2", crossorigin: "true" }, 102 | ], 103 | ["gif", { rel: "preload", as: "image", type: "image/gif" }], 104 | ["jpg", { rel: "preload", as: "image", type: "image/jpeg" }], 105 | ["jpeg", { rel: "preload", as: "image", type: "image/jpeg" }], 106 | ["png", { rel: "preload", as: "image", type: "image/png" }], 107 | ["webp", { rel: "preload", as: "image", type: "image/webp" }], 108 | ["svg", { rel: "preload", as: "image", type: "image/svg+xml" }], 109 | ["ico", { rel: "preload", as: "image", type: "image/x-icon" }], 110 | ["avif", { rel: "preload", as: "image", type: "image/avif" }], 111 | ["mp4", { rel: "preload", as: "video", type: "video/mp4" }], 112 | ["webm", { rel: "preload", as: "video", type: "video/webm" }], 113 | ]); 114 | 115 | /** 116 | * Generates a link tag for a given file. This will load stylesheets and preload 117 | * everything else. It uses the file extension to determine the type. 118 | */ 119 | 120 | export function renderLinkTag(file: string) { 121 | const ext = basename(file).split(".").pop(); 122 | if (!ext) { 123 | return ""; 124 | } 125 | const props = linkPropsMap.get(ext); 126 | if (!props) { 127 | return ""; 128 | } 129 | const attrs = Object.entries(props) 130 | .map(([key, value]) => `${key}="${value}"`) 131 | .join(" "); 132 | return ``; 133 | } 134 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ModuleImports = Record< 2 | string, 3 | () => Promise 4 | >; 5 | 6 | export interface Context { 7 | path: string; 8 | chunk: string; 9 | data?: TData; 10 | routeData?: TRouteData; 11 | params?: Record; 12 | assets?: Array; 13 | } 14 | 15 | export interface RouteModule { 16 | default: TElement; 17 | } 18 | 19 | export interface StaticDataModule { 20 | getRouteData?: () => Promise | TRouteData; 21 | } 22 | 23 | export interface DynamicDataModule< 24 | TPathData extends Record = Record, 25 | TRouteData extends Record = Record 26 | > extends StaticDataModule { 27 | getStaticPaths: () => 28 | | Promise<{ paths: Array> }> 29 | | { paths: Array> }; 30 | } 31 | 32 | export interface PathInfo> { 33 | params: Record; 34 | data?: TData; 35 | } 36 | 37 | export type RouteModuleFunction = () => Promise< 38 | RouteModule 39 | >; 40 | 41 | export interface ServerEntry { 42 | routeModules: ModuleImports>; 43 | dataModules: ModuleImports; 44 | render( 45 | context: Context, 46 | mod: RouteModuleFunction, 47 | bootstrapModules?: Array 48 | ): Promise<{ 49 | body: string; 50 | head: string | false; 51 | }>; 52 | } 53 | 54 | export type DataModule = DynamicDataModule; 55 | 56 | export type DataType = 57 | StaticPaths["paths"][number]; 58 | 59 | export type ReturnTypeIfDefined any) | undefined> = 60 | T extends undefined ? undefined : ReturnType>; 61 | 62 | export type AwaitedIfPromise = T extends PromiseLike ? Awaited : T; 63 | export type StaticPaths = AwaitedIfPromise< 64 | ReturnTypeIfDefined 65 | >; 66 | 67 | export type RouteData = AwaitedIfPromise< 68 | ReturnTypeIfDefined 69 | >; 70 | 71 | export interface StaticRouteProps< 72 | Mod extends StaticDataModule | undefined = undefined 73 | > { 74 | path: string; 75 | routeData?: Mod extends undefined 76 | ? undefined 77 | : RouteData>; 78 | } 79 | 80 | export interface DynamicRouteProps 81 | extends StaticRouteProps { 82 | params: DataType["params"]; 83 | data: DataType["data"]; 84 | } 85 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "ESNext", 7 | "DOM" 8 | ], 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "esModuleInterop": false, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "ESNext", 16 | "moduleResolution": "Node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react-jsx" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } -------------------------------------------------------------------------------- /packages/create-impala/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.8](https://github.com/ascorbic/impala/compare/create-impala-v0.0.7...create-impala-v0.0.8) (2024-12-09) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * support Vite 6 ([#39](https://github.com/ascorbic/impala/issues/39)) ([2221687](https://github.com/ascorbic/impala/commit/2221687544d533570f86b25848cf86d80b34f08d)) 9 | 10 | ## [0.0.7](https://github.com/ascorbic/impala/compare/create-impala-v0.0.6...create-impala-v0.0.7) (2023-04-02) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * Ensure users can exit from framework prompt step ([#16](https://github.com/ascorbic/impala/issues/16)) ([7c84501](https://github.com/ascorbic/impala/commit/7c84501bf5a06d5672fb95d10ade16bdec8ff4b7)) 16 | 17 | ## [0.0.6](https://github.com/ascorbic/impala/compare/create-impala-v0.0.5...create-impala-v0.0.6) (2023-03-28) 18 | 19 | 20 | ### Features 21 | 22 | * add preact support ([#9](https://github.com/ascorbic/impala/issues/9)) ([ef71048](https://github.com/ascorbic/impala/commit/ef710486657819cbf6addaa1aaff671931b5ed4f)) 23 | 24 | ## 0.0.5 (2023-03-28) 25 | 26 | 27 | ### Features 28 | 29 | * add support for JavaScript ([#8](https://github.com/ascorbic/impala/issues/8)) ([ea5d1fa](https://github.com/ascorbic/impala/commit/ea5d1fa59623ae70c3ead2b58d5076e5d6605c74)) 30 | * **create-impala:** add create-impala cli ([d40b61c](https://github.com/ascorbic/impala/commit/d40b61c469223bc88d62fce156790ecaf2090e49)) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * fix cli wrapper ([80172b2](https://github.com/ascorbic/impala/commit/80172b2cdc146ae2b248b79f20eb4cd98ea89b40)) 36 | -------------------------------------------------------------------------------- /packages/create-impala/create-impala.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const currentVersion = process.versions.node; 3 | const currentMajor = parseInt(currentVersion.split(".")[0], 10); 4 | const minimumMajorVersion = 16; 5 | 6 | if (currentMajor < minimumMajorVersion) { 7 | console.error(`Node.js v${currentVersion} is unsupported!`); 8 | console.error(`Please use Node.js v${minimumMajorVersion} or higher.`); 9 | process.exit(1); 10 | } 11 | 12 | import("./dist/cli.js").then(({ createImpala }) => createImpala()); 13 | -------------------------------------------------------------------------------- /packages/create-impala/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-impala", 3 | "version": "0.0.8", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "create-impala", 9 | "version": "0.0.8", 10 | "license": "MIT", 11 | "bin": { 12 | "create-impala": "dist/cli.js" 13 | }, 14 | "devDependencies": { 15 | "@clack/prompts": "^0.6.3", 16 | "degit": "^2.8.4", 17 | "tsup": "^6.7.0" 18 | } 19 | }, 20 | "../../node_modules/.pnpm/@clack+prompts@0.6.3/node_modules/@clack/prompts": { 21 | "version": "0.6.3", 22 | "bundleDependencies": [ 23 | "is-unicode-supported" 24 | ], 25 | "dev": true, 26 | "license": "MIT", 27 | "dependencies": { 28 | "@clack/core": "^0.3.2", 29 | "picocolors": "^1.0.0", 30 | "sisteransi": "^1.0.5" 31 | }, 32 | "devDependencies": { 33 | "is-unicode-supported": "^1.3.0" 34 | } 35 | }, 36 | "../../node_modules/.pnpm/@clack+prompts@0.6.3/node_modules/@clack/prompts/node_modules/is-unicode-supported": { 37 | "version": "1.3.0", 38 | "dev": true, 39 | "inBundle": true, 40 | "license": "MIT", 41 | "engines": { 42 | "node": ">=12" 43 | }, 44 | "funding": { 45 | "url": "https://github.com/sponsors/sindresorhus" 46 | } 47 | }, 48 | "../../node_modules/.pnpm/tsup@6.7.0/node_modules/tsup": { 49 | "version": "6.7.0", 50 | "dev": true, 51 | "license": "MIT", 52 | "dependencies": { 53 | "bundle-require": "^4.0.0", 54 | "cac": "^6.7.12", 55 | "chokidar": "^3.5.1", 56 | "debug": "^4.3.1", 57 | "esbuild": "^0.17.6", 58 | "execa": "^5.0.0", 59 | "globby": "^11.0.3", 60 | "joycon": "^3.0.1", 61 | "postcss-load-config": "^3.0.1", 62 | "resolve-from": "^5.0.0", 63 | "rollup": "^3.2.5", 64 | "source-map": "0.8.0-beta.0", 65 | "sucrase": "^3.20.3", 66 | "tree-kill": "^1.2.2" 67 | }, 68 | "bin": { 69 | "tsup": "dist/cli-default.js", 70 | "tsup-node": "dist/cli-node.js" 71 | }, 72 | "devDependencies": { 73 | "@rollup/plugin-json": "5.0.1", 74 | "@swc/core": "1.2.218", 75 | "@types/debug": "4.1.7", 76 | "@types/flat": "5.0.2", 77 | "@types/fs-extra": "9.0.13", 78 | "@types/node": "14.18.12", 79 | "@types/resolve": "1.20.1", 80 | "colorette": "2.0.16", 81 | "consola": "2.15.3", 82 | "flat": "5.0.2", 83 | "fs-extra": "10.0.0", 84 | "postcss": "8.4.12", 85 | "postcss-simple-vars": "6.0.3", 86 | "prettier": "2.5.1", 87 | "resolve": "1.20.0", 88 | "rollup-plugin-dts": "5.3.0", 89 | "rollup-plugin-hashbang": "2.2.2", 90 | "strip-json-comments": "4.0.0", 91 | "svelte": "3.46.4", 92 | "terser": "^5.16.0", 93 | "ts-essentials": "9.1.2", 94 | "tsconfig-paths": "3.12.0", 95 | "tsup": "6.6.1", 96 | "typescript": "5.0.2", 97 | "vitest": "0.28.4", 98 | "wait-for-expect": "3.0.2" 99 | }, 100 | "engines": { 101 | "node": ">=14.18" 102 | }, 103 | "peerDependencies": { 104 | "@swc/core": "^1", 105 | "postcss": "^8.4.12", 106 | "typescript": ">=4.1.0" 107 | }, 108 | "peerDependenciesMeta": { 109 | "@swc/core": { 110 | "optional": true 111 | }, 112 | "postcss": { 113 | "optional": true 114 | }, 115 | "typescript": { 116 | "optional": true 117 | } 118 | } 119 | }, 120 | "node_modules/@clack/prompts": { 121 | "resolved": "../../node_modules/.pnpm/@clack+prompts@0.6.3/node_modules/@clack/prompts", 122 | "link": true 123 | }, 124 | "node_modules/degit": { 125 | "version": "2.8.4", 126 | "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", 127 | "integrity": "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==", 128 | "dev": true, 129 | "bin": { 130 | "degit": "degit" 131 | }, 132 | "engines": { 133 | "node": ">=8.0.0" 134 | } 135 | }, 136 | "node_modules/tsup": { 137 | "resolved": "../../node_modules/.pnpm/tsup@6.7.0/node_modules/tsup", 138 | "link": true 139 | } 140 | }, 141 | "dependencies": { 142 | "@clack/prompts": { 143 | "version": "file:../../node_modules/.pnpm/@clack+prompts@0.6.3/node_modules/@clack/prompts", 144 | "requires": { 145 | "@clack/core": "^0.3.2", 146 | "is-unicode-supported": "^1.3.0", 147 | "picocolors": "^1.0.0", 148 | "sisteransi": "^1.0.5" 149 | }, 150 | "dependencies": { 151 | "is-unicode-supported": { 152 | "version": "1.3.0", 153 | "bundled": true, 154 | "dev": true 155 | } 156 | } 157 | }, 158 | "degit": { 159 | "version": "2.8.4", 160 | "resolved": "https://registry.npmjs.org/degit/-/degit-2.8.4.tgz", 161 | "integrity": "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng==", 162 | "dev": true 163 | }, 164 | "tsup": { 165 | "version": "file:../../node_modules/.pnpm/tsup@6.7.0/node_modules/tsup", 166 | "requires": { 167 | "@rollup/plugin-json": "5.0.1", 168 | "@swc/core": "1.2.218", 169 | "@types/debug": "4.1.7", 170 | "@types/flat": "5.0.2", 171 | "@types/fs-extra": "9.0.13", 172 | "@types/node": "14.18.12", 173 | "@types/resolve": "1.20.1", 174 | "bundle-require": "^4.0.0", 175 | "cac": "^6.7.12", 176 | "chokidar": "^3.5.1", 177 | "colorette": "2.0.16", 178 | "consola": "2.15.3", 179 | "debug": "^4.3.1", 180 | "esbuild": "^0.17.6", 181 | "execa": "^5.0.0", 182 | "flat": "5.0.2", 183 | "fs-extra": "10.0.0", 184 | "globby": "^11.0.3", 185 | "joycon": "^3.0.1", 186 | "postcss": "8.4.12", 187 | "postcss-load-config": "^3.0.1", 188 | "postcss-simple-vars": "6.0.3", 189 | "prettier": "2.5.1", 190 | "resolve": "1.20.0", 191 | "resolve-from": "^5.0.0", 192 | "rollup": "^3.2.5", 193 | "rollup-plugin-dts": "5.3.0", 194 | "rollup-plugin-hashbang": "2.2.2", 195 | "source-map": "0.8.0-beta.0", 196 | "strip-json-comments": "4.0.0", 197 | "sucrase": "^3.20.3", 198 | "svelte": "3.46.4", 199 | "terser": "^5.16.0", 200 | "tree-kill": "^1.2.2", 201 | "ts-essentials": "9.1.2", 202 | "tsconfig-paths": "3.12.0", 203 | "tsup": "6.6.1", 204 | "typescript": "5.0.2", 205 | "vitest": "0.28.4", 206 | "wait-for-expect": "3.0.2" 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /packages/create-impala/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-impala", 3 | "version": "0.0.8", 4 | "description": "", 5 | "bin": { 6 | "create-impala": "create-impala.js" 7 | }, 8 | "scripts": { 9 | "build": "tsup --format cjs src/cli.ts --clean" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@clack/prompts": "^0.6.3", 16 | "@types/node": "^18.15.7", 17 | "degit": "^2.8.4", 18 | "picocolors": "^1.0.0", 19 | "tsup": "^6.7.0" 20 | }, 21 | "engines": { 22 | "node": ">=16.0.0" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/create-impala/src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | declare module "degit"; 2 | -------------------------------------------------------------------------------- /packages/create-impala/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { intro, outro, spinner, text, isCancel, select } from "@clack/prompts"; 2 | import degit from "degit"; 3 | import { existsSync, promises as fs } from "node:fs"; 4 | import { join } from "node:path"; 5 | import color from "picocolors"; 6 | 7 | export async function createImpala() { 8 | intro(color.black(color.bgYellow(" create-impala "))); 9 | 10 | const target = await text({ 11 | message: "Where would you like to create your project?", 12 | placeholder: "my-impala-app", 13 | initialValue: "my-impala-app", 14 | validate(value) { 15 | if (existsSync(value)) { 16 | return "That folder already exists! Please choose another."; 17 | } 18 | }, 19 | }); 20 | if (isCancel(target)) { 21 | outro("Cancelled"); 22 | return; 23 | } 24 | 25 | const language = await select({ 26 | message: "Which language would you like to use?", 27 | options: [ 28 | { value: "ts", label: "TypeScript" }, 29 | { value: "js", label: "JavaScript" }, 30 | ], 31 | }); 32 | 33 | if (isCancel(language)) { 34 | outro("Cancelled"); 35 | return; 36 | } 37 | 38 | const framework = await select({ 39 | message: "Which framework would you like to use?", 40 | options: [ 41 | { value: "react", label: "React" }, 42 | { value: "preact", label: "Preact" }, 43 | ], 44 | }); 45 | 46 | if (isCancel(framework)) { 47 | outro("Cancelled"); 48 | return; 49 | } 50 | 51 | const s = spinner(); 52 | 53 | s.start("Setting up your project..."); 54 | 55 | const emitter = degit(`ascorbic/impala/templates/${framework}-${language}`, { 56 | cache: false, 57 | }); 58 | 59 | emitter.on("info", (info: any) => { 60 | console.log(info.message); 61 | }); 62 | 63 | emitter.on("error", (error: any) => { 64 | console.log(error.message); 65 | }); 66 | 67 | await emitter.clone(target); 68 | s.stop("Set up your project"); 69 | const packageJsonPath = join(target, "package.json"); 70 | const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); 71 | packageJson.name = target.replaceAll(/[^a-zA-Z0-9-]/g, "-"); 72 | await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); 73 | outro( 74 | `You're all set!\n\nTo get started, run:\n\n cd ${target}\n npm install\n npm run dev` 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /packages/create-impala/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "CommonJS", /* Specify what module code is generated. */ 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "resolveJsonModule": true, /* Enable importing .json files. */ 36 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 37 | /* JavaScript Support */ 38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 41 | /* Emit */ 42 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 47 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 48 | // "removeComments": true, /* Disable emitting comments. */ 49 | // "noEmit": true, /* Disable emitting files from a compilation. */ 50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 58 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 59 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 62 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 64 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 65 | /* Interop Constraints */ 66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 68 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 70 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 71 | /* Type Checking */ 72 | "strict": true, /* Enable all strict type-checking options. */ 73 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 74 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 75 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 76 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 77 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 78 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 79 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 80 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 81 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 82 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 83 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 84 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 85 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 86 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 87 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 88 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 89 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 90 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 91 | /* Completeness */ 92 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 93 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 94 | } 95 | } -------------------------------------------------------------------------------- /packages/preact/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Dependencies 4 | 5 | * The following workspace dependencies were updated 6 | * dependencies 7 | * @impalajs/core bumped to 0.0.6 8 | 9 | ### Dependencies 10 | 11 | * The following workspace dependencies were updated 12 | * dependencies 13 | * @impalajs/core bumped to 0.0.7 14 | 15 | ### Dependencies 16 | 17 | * The following workspace dependencies were updated 18 | * dependencies 19 | * @impalajs/core bumped to 0.0.8 20 | 21 | ### Dependencies 22 | 23 | * The following workspace dependencies were updated 24 | * dependencies 25 | * @impalajs/core bumped to 0.0.9 26 | 27 | ### Dependencies 28 | 29 | * The following workspace dependencies were updated 30 | * dependencies 31 | * @impalajs/core bumped to 0.0.10 32 | 33 | ### Dependencies 34 | 35 | * The following workspace dependencies were updated 36 | * dependencies 37 | * @impalajs/core bumped to 0.0.11 38 | 39 | ### Dependencies 40 | 41 | * The following workspace dependencies were updated 42 | * dependencies 43 | * @impalajs/core bumped to 0.0.12 44 | 45 | ### Dependencies 46 | 47 | * The following workspace dependencies were updated 48 | * dependencies 49 | * @impalajs/core bumped to 0.0.13 50 | 51 | ### Dependencies 52 | 53 | * The following workspace dependencies were updated 54 | * dependencies 55 | * @impalajs/core bumped to 0.0.14 56 | 57 | ## 0.0.5 (2023-03-28) 58 | 59 | 60 | ### Features 61 | 62 | * add preact support ([#9](https://github.com/ascorbic/impala/issues/9)) ([ef71048](https://github.com/ascorbic/impala/commit/ef710486657819cbf6addaa1aaff671931b5ed4f)) 63 | 64 | ## 0.0.5 (2023-03-28) 65 | 66 | 67 | ### Features 68 | 69 | * add dev server ([#2](https://github.com/ascorbic/impala/issues/2)) ([bab917a](https://github.com/ascorbic/impala/commit/bab917a28df70d9df691f7d1db61bf6e140b7acb)) 70 | * **create-impala:** add create-impala cli ([d40b61c](https://github.com/ascorbic/impala/commit/d40b61c469223bc88d62fce156790ecaf2090e49)) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * pass props to pages properly ([6b18945](https://github.com/ascorbic/impala/commit/6b189453d821ad85fdf828f5d270c754fecb0b26)) 76 | * pass props to pages properly ([#3](https://github.com/ascorbic/impala/issues/3)) ([ee3bb82](https://github.com/ascorbic/impala/commit/ee3bb8279987dcdd0655ef02a53bad883ee3413a)) 77 | 78 | 79 | ### Dependencies 80 | 81 | * The following workspace dependencies were updated 82 | * dependencies 83 | * @impalajs/core bumped to 0.0.5 84 | -------------------------------------------------------------------------------- /packages/preact/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 | ![impala](https://user-images.githubusercontent.com/213306/227727009-a4dc391f-efb1-4489-ad73-c3d3a327704a.png) 6 | 7 |

8 | -------------------------------------------------------------------------------- /packages/preact/client.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/client"; 2 | -------------------------------------------------------------------------------- /packages/preact/head.d.ts: -------------------------------------------------------------------------------- 1 | export { Head } from "./dist/head"; 2 | -------------------------------------------------------------------------------- /packages/preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@impalajs/preact", 3 | "version": "0.0.14", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsup src/index.ts src/client.tsx src/head.tsx --format cjs,esm --dts --clean" 7 | }, 8 | "main": "./dist/index.js", 9 | "module": "./dist/index.mjs", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs" 16 | }, 17 | "./client": { 18 | "types": "./dist/client.d.ts", 19 | "require": "./dist/client.js", 20 | "import": "./dist/client.mjs" 21 | }, 22 | "./head": { 23 | "types": "./dist/head.d.ts", 24 | "require": "./dist/head.js", 25 | "import": "./dist/head.mjs" 26 | } 27 | }, 28 | "keywords": [], 29 | "author": "", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@impalajs/core": "workspace:*", 33 | "preact-render-to-string": "^6.0.1" 34 | }, 35 | "devDependencies": { 36 | "preact": "^10.13.2", 37 | "tsup": "^6.7.0" 38 | }, 39 | "peerDependencies": { 40 | "preact": "^10.0.0" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } -------------------------------------------------------------------------------- /packages/preact/src/client.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ModuleImports, 3 | RouteModule as CoreRouteModule, 4 | } from "@impalajs/core"; 5 | import { hydrate, ComponentType } from "preact"; 6 | 7 | export type RouteModule = CoreRouteModule; 8 | export function clientBootstrap(modules: ModuleImports) { 9 | const context = (window as any).___CONTEXT; 10 | 11 | if (context && "chunk" in context) { 12 | const mod = modules[context.chunk]; 13 | if (mod) { 14 | mod().then(({ default: Page }) => { 15 | hydrate(, document.getElementById("__impala")!); 16 | }); 17 | } else { 18 | console.error( 19 | `[Impala] Could not hydrate page. Module not found: ${context?.chunk}` 20 | ); 21 | } 22 | } else { 23 | console.log("[Impala] No context found. Skipping hydration."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/preact/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "preact/hooks"; 2 | import renderToString from "preact-render-to-string"; 3 | import type { ComponentType } from "preact"; 4 | import type { Context, RouteModule } from "@impalajs/core"; 5 | import { HeadContext } from "./head-context"; 6 | 7 | function HeadContent() { 8 | const headProvider = useContext(HeadContext); 9 | return <>{...headProvider.getHead()}; 10 | } 11 | 12 | export async function render( 13 | context: Context, 14 | mod: () => Promise>>, 15 | bootstrapModules?: Array 16 | ) { 17 | const { default: Page } = await mod(); 18 | 19 | const body = renderToString(); 20 | 21 | const modules = bootstrapModules?.map( 22 | (m) => `` 23 | ); 24 | 25 | const headContent = renderToString(); 26 | 27 | return { 28 | body, 29 | head: [ 30 | headContent, 31 | ``, 32 | modules, 33 | ].join(""), 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/preact/src/head-context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, VNode } from "preact"; 2 | import render from "preact-render-to-string"; 3 | 4 | class HeadProvider { 5 | private head: VNode[] = []; 6 | 7 | private removeTag(tag: string) { 8 | this.head = this.head.filter((item) => item.type !== tag); 9 | } 10 | 11 | public addHead(head?: VNode | VNode[]) { 12 | if (!head) { 13 | return; 14 | } 15 | if (Array.isArray(head)) { 16 | // Can't have more than one title tag 17 | if (head.some((item) => item.type === "title")) { 18 | this.removeTag("title"); 19 | } 20 | this.head.push(...head); 21 | return; 22 | } 23 | 24 | if (head.type === "title") { 25 | this.removeTag("title"); 26 | } 27 | this.head.push(head); 28 | } 29 | 30 | public getHead() { 31 | return this.head; 32 | } 33 | 34 | public getAsString() { 35 | return render(<>{...this.head}); 36 | } 37 | } 38 | 39 | const headProvider = new HeadProvider(); 40 | 41 | export const HeadContext = createContext(headProvider); 42 | -------------------------------------------------------------------------------- /packages/preact/src/head.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "preact/hooks"; 2 | import type { FunctionComponent, VNode } from "preact"; 3 | import { HeadContext } from "./head-context"; 4 | 5 | interface HeadProps { 6 | title?: string; 7 | children?: VNode | VNode[]; 8 | } 9 | 10 | export const Head: FunctionComponent = ({ children, title }) => { 11 | const headProvider = useContext(HeadContext); 12 | headProvider.addHead(children); 13 | if (title) { 14 | headProvider.addHead({title}); 15 | } 16 | return null; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/preact/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DataModule, 3 | RouteModule as CoreRouteModule, 4 | } from "@impalajs/core"; 5 | import type { ComponentChildren } from "preact"; 6 | export type RouteModule = CoreRouteModule; 7 | export type { DataModule }; 8 | 9 | export * from "./entry-server"; 10 | -------------------------------------------------------------------------------- /packages/preact/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "types": [], 11 | "allowJs": false, 12 | "skipLibCheck": true, 13 | "esModuleInterop": false, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "ESNext", 18 | "moduleResolution": "Node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "jsxImportSource": "preact" 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "references": [ 29 | { 30 | "path": "./tsconfig.node.json" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /packages/react/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### Dependencies 4 | 5 | * The following workspace dependencies were updated 6 | * dependencies 7 | * @impalajs/core bumped to 0.0.6 8 | 9 | ### Dependencies 10 | 11 | * The following workspace dependencies were updated 12 | * dependencies 13 | * @impalajs/core bumped to 0.0.7 14 | 15 | ### Dependencies 16 | 17 | * The following workspace dependencies were updated 18 | * dependencies 19 | * @impalajs/core bumped to 0.0.8 20 | 21 | ### Dependencies 22 | 23 | * The following workspace dependencies were updated 24 | * dependencies 25 | * @impalajs/core bumped to 0.0.9 26 | 27 | ### Dependencies 28 | 29 | * The following workspace dependencies were updated 30 | * dependencies 31 | * @impalajs/core bumped to 0.0.10 32 | 33 | ### Dependencies 34 | 35 | * The following workspace dependencies were updated 36 | * dependencies 37 | * @impalajs/core bumped to 0.0.11 38 | 39 | ### Dependencies 40 | 41 | * The following workspace dependencies were updated 42 | * dependencies 43 | * @impalajs/core bumped to 0.0.12 44 | 45 | ### Dependencies 46 | 47 | * The following workspace dependencies were updated 48 | * dependencies 49 | * @impalajs/core bumped to 0.0.13 50 | 51 | ### Dependencies 52 | 53 | * The following workspace dependencies were updated 54 | * dependencies 55 | * @impalajs/core bumped to 0.0.14 56 | 57 | ## 0.0.5 (2023-03-28) 58 | 59 | 60 | ### Features 61 | 62 | * add dev server ([#2](https://github.com/ascorbic/impala/issues/2)) ([bab917a](https://github.com/ascorbic/impala/commit/bab917a28df70d9df691f7d1db61bf6e140b7acb)) 63 | * **create-impala:** add create-impala cli ([d40b61c](https://github.com/ascorbic/impala/commit/d40b61c469223bc88d62fce156790ecaf2090e49)) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * pass props to pages properly ([6b18945](https://github.com/ascorbic/impala/commit/6b189453d821ad85fdf828f5d270c754fecb0b26)) 69 | * pass props to pages properly ([#3](https://github.com/ascorbic/impala/issues/3)) ([ee3bb82](https://github.com/ascorbic/impala/commit/ee3bb8279987dcdd0655ef02a53bad883ee3413a)) 70 | 71 | 72 | ### Dependencies 73 | 74 | * The following workspace dependencies were updated 75 | * dependencies 76 | * @impalajs/core bumped to 0.0.5 77 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 | ![impala](https://user-images.githubusercontent.com/213306/227727009-a4dc391f-efb1-4489-ad73-c3d3a327704a.png) 6 | 7 |

8 | -------------------------------------------------------------------------------- /packages/react/client.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/client"; 2 | -------------------------------------------------------------------------------- /packages/react/head.d.ts: -------------------------------------------------------------------------------- 1 | export { Head } from "./dist/head"; 2 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@impalajs/react", 3 | "version": "0.0.14", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsup src/index.ts src/client.tsx src/head.tsx --format cjs,esm --dts --clean" 7 | }, 8 | "main": "./dist/index.js", 9 | "module": "./dist/index.mjs", 10 | "types": "./dist/index.d.ts", 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "require": "./dist/index.js", 15 | "import": "./dist/index.mjs" 16 | }, 17 | "./client": { 18 | "types": "./dist/client.d.ts", 19 | "require": "./dist/client.js", 20 | "import": "./dist/client.mjs" 21 | }, 22 | "./head": { 23 | "types": "./dist/head.d.ts", 24 | "require": "./dist/head.js", 25 | "import": "./dist/head.mjs" 26 | } 27 | }, 28 | "keywords": [], 29 | "author": "", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@impalajs/core": "workspace:*" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^18.15.7", 36 | "@types/react": "^18.0.28", 37 | "@types/react-dom": "^18.0.11", 38 | "react": "^18.2.0", 39 | "react-dom": "^18.2.0", 40 | "tsup": "^6.7.0" 41 | }, 42 | "peerDependencies": { 43 | "react": ">=18", 44 | "react-dom": ">=18" 45 | }, 46 | "publishConfig": { 47 | "access": "public" 48 | } 49 | } -------------------------------------------------------------------------------- /packages/react/src/client.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ModuleImports, 3 | RouteModule as CoreRouteModule, 4 | } from "@impalajs/core"; 5 | import type { ElementType } from "react"; 6 | import ReactDOM from "react-dom/client"; 7 | 8 | export type RouteModule = CoreRouteModule; 9 | export function clientBootstrap(modules: ModuleImports) { 10 | const context = (window as any).___CONTEXT; 11 | 12 | if (context && "chunk" in context) { 13 | const mod = modules[context.chunk]; 14 | if (mod) { 15 | mod().then(({ default: Page }) => { 16 | ReactDOM.hydrateRoot( 17 | document.getElementById("__impala")!, 18 | 19 | ); 20 | }); 21 | } else { 22 | console.error( 23 | `[Impala] Could not hydrate page. Module not found: ${context?.chunk}` 24 | ); 25 | } 26 | } else { 27 | console.log("[Impala] No context found. Skipping hydration."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/react/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToPipeableStream, renderToStaticMarkup } from "react-dom/server"; 2 | import { ElementType, useContext } from "react"; 3 | import type { Context, RouteModule } from "@impalajs/core"; 4 | import { Writable, WritableOptions } from "node:stream"; 5 | import { HeadContext } from "./head-context"; 6 | 7 | class StringResponse extends Writable { 8 | private buffer: string; 9 | private responseData: Promise; 10 | constructor(options?: WritableOptions) { 11 | super(options); 12 | this.buffer = ""; 13 | this.responseData = new Promise((resolve, reject) => { 14 | this.on("finish", () => resolve(this.buffer)); 15 | this.on("error", reject); 16 | }); 17 | } 18 | 19 | _write( 20 | chunk: any, 21 | encoding: BufferEncoding, 22 | callback: (error?: Error | null) => void 23 | ): void { 24 | this.buffer += chunk; 25 | callback(); 26 | } 27 | 28 | getData(): Promise { 29 | return this.responseData; 30 | } 31 | } 32 | 33 | function HeadContent() { 34 | const headProvider = useContext(HeadContext); 35 | return <>{...headProvider.getHead()}; 36 | } 37 | 38 | export async function render( 39 | context: Context, 40 | mod: () => Promise>, 41 | bootstrapModules?: Array 42 | ) { 43 | const { default: Page } = await mod(); 44 | 45 | const response = new StringResponse(); 46 | 47 | const { pipe } = renderToPipeableStream(, { 48 | bootstrapModules, 49 | bootstrapScriptContent: `window.___CONTEXT=${JSON.stringify(context)};`, 50 | onAllReady() { 51 | pipe(response); 52 | }, 53 | onError(error) { 54 | console.error(error); 55 | }, 56 | }); 57 | 58 | const body = await response.getData(); 59 | 60 | const head = renderToStaticMarkup(); 61 | 62 | return { body, head }; 63 | } 64 | -------------------------------------------------------------------------------- /packages/react/src/head-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactElement, ReactNode } from "react"; 2 | import { renderToStaticMarkup } from "react-dom/server"; 3 | 4 | class HeadProvider { 5 | private head: React.ReactElement[] = []; 6 | 7 | private removeTag(tag: string) { 8 | this.head = this.head.filter((item) => item.type !== tag); 9 | } 10 | 11 | public addHead(head?: ReactElement | ReactElement[]) { 12 | if (!head) { 13 | return; 14 | } 15 | if (Array.isArray(head)) { 16 | // Can't have more than one title tag 17 | if (head.some((item) => item.type === "title")) { 18 | this.removeTag("title"); 19 | } 20 | this.head.push(...head); 21 | return; 22 | } 23 | 24 | if (head.type === "title") { 25 | this.removeTag("title"); 26 | } 27 | this.head.push(head); 28 | } 29 | 30 | public getHead() { 31 | return this.head; 32 | } 33 | 34 | public getAsString() { 35 | return renderToStaticMarkup(<>{...this.head}); 36 | } 37 | } 38 | 39 | const headProvider = new HeadProvider(); 40 | 41 | export const HeadContext = createContext(headProvider); 42 | -------------------------------------------------------------------------------- /packages/react/src/head.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { HeadContext } from "./head-context"; 3 | 4 | interface HeadProps { 5 | title?: string; 6 | children?: React.ReactElement | React.ReactElement[]; 7 | } 8 | 9 | export const Head: React.FC = ({ children, title }) => { 10 | const headProvider = useContext(HeadContext); 11 | headProvider.addHead(children); 12 | if (title) { 13 | headProvider.addHead({title}); 14 | } 15 | return null; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DataModule, 3 | ModuleImports, 4 | RouteModule as CoreRouteModule, 5 | } from "@impalajs/core"; 6 | import type { ElementType } from "react"; 7 | export type RouteModule = CoreRouteModule; 8 | export type { DataModule }; 9 | 10 | export * from "./entry-server"; 11 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - templates/* 3 | - packages/* 4 | - demo 5 | - scripts 6 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "packages/core": {}, 4 | "packages/preact": {}, 5 | "packages/react": {}, 6 | "packages/create-impala": {} 7 | }, 8 | "plugins": [ 9 | "node-workspace" 10 | ], 11 | "bump-minor-pre-major": true, 12 | "bump-patch-for-minor-pre-major": true, 13 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 14 | } -------------------------------------------------------------------------------- /scripts/compile-template.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mostly copied from Shopify/Hydrogen 3 | */ 4 | 5 | import path from "path"; 6 | import fs from "fs/promises"; 7 | import prettier, { type Options as FormatOptions } from "prettier"; 8 | import ts, { type CompilerOptions } from "typescript"; 9 | import glob from "fast-glob"; 10 | import { existsSync } from "fs"; 11 | 12 | const escapeNewLines = (code: string) => 13 | code.replace(/\n\n/g, "\n/* :newline: */"); 14 | const restoreNewLines = (code: string) => 15 | code.replace(/\/\* :newline: \*\//g, "\n"); 16 | 17 | const DEFAULT_TS_CONFIG: Omit = { 18 | lib: ["DOM", "DOM.Iterable", "ES2022"], 19 | isolatedModules: true, 20 | esModuleInterop: true, 21 | resolveJsonModule: true, 22 | target: "ES2022", 23 | strict: true, 24 | allowJs: true, 25 | forceConsistentCasingInFileNames: true, 26 | skipLibCheck: true, 27 | }; 28 | 29 | export function transpileFile(code: string, config = DEFAULT_TS_CONFIG) { 30 | // We need to escape new lines in the template because TypeScript 31 | // will remove them when compiling. 32 | const withArtificialNewLines = escapeNewLines(code); 33 | 34 | // We compile the template to JavaScript. 35 | const compiled = ts.transpileModule(withArtificialNewLines, { 36 | reportDiagnostics: false, 37 | compilerOptions: { 38 | ...config, 39 | // '1' tells TypeScript to preserve the JSX syntax. 40 | jsx: 1, 41 | removeComments: false, 42 | }, 43 | }); 44 | 45 | // Here we restore the new lines that were removed by TypeScript. 46 | return restoreNewLines(compiled.outputText); 47 | } 48 | 49 | const DEFAULT_PRETTIER_CONFIG: FormatOptions = { 50 | arrowParens: "always", 51 | singleQuote: true, 52 | bracketSpacing: false, 53 | trailingComma: "all", 54 | }; 55 | 56 | export async function resolveFormatConfig(filePath = process.cwd()) { 57 | try { 58 | // Try to read a prettier config file from the project. 59 | return (await prettier.resolveConfig(filePath)) || DEFAULT_PRETTIER_CONFIG; 60 | } catch { 61 | return DEFAULT_PRETTIER_CONFIG; 62 | } 63 | } 64 | 65 | export function format(content: string, config: FormatOptions, filePath = "") { 66 | const ext = path.extname(filePath); 67 | 68 | const formattedContent = prettier.format(content, { 69 | // Specify the TypeScript parser for ts/tsx files. Otherwise 70 | // we need to use the babel parser because the default parser 71 | // Otherwise prettier will print a warning. 72 | parser: ext === ".tsx" || ext === ".ts" ? "typescript" : "babel", 73 | ...config, 74 | }); 75 | 76 | return formattedContent; 77 | } 78 | 79 | const DEFAULT_JS_CONFIG: Omit = { 80 | allowJs: true, 81 | forceConsistentCasingInFileNames: true, 82 | strict: true, 83 | lib: ["DOM", "DOM.Iterable", "ES2022"], 84 | esModuleInterop: true, 85 | isolatedModules: true, 86 | jsx: "react-jsx", 87 | noEmit: true, 88 | resolveJsonModule: true, 89 | }; 90 | 91 | // https://code.visualstudio.com/docs/languages/jsconfig#_jsconfig-options 92 | const JS_CONFIG_KEYS = [ 93 | "noLib", 94 | "target", 95 | "module", 96 | "moduleResolution", 97 | "checkJs", 98 | "experimentalDecorators", 99 | "allowSyntheticDefaultImports", 100 | "baseUrl", 101 | "paths", 102 | ...Object.keys(DEFAULT_JS_CONFIG), 103 | ]; 104 | 105 | export function convertConfigToJS(tsConfig: { 106 | include?: string[]; 107 | compilerOptions?: CompilerOptions; 108 | }) { 109 | const jsConfig = { 110 | compilerOptions: { ...DEFAULT_JS_CONFIG }, 111 | } as typeof tsConfig; 112 | 113 | if (tsConfig.include) { 114 | jsConfig.include = tsConfig.include 115 | .filter((s) => !s.endsWith(".d.ts")) 116 | .map((s) => s.replace(/\.ts(x?)$/, ".js$1")); 117 | } 118 | 119 | if (tsConfig.compilerOptions) { 120 | for (const key of JS_CONFIG_KEYS) { 121 | if (tsConfig.compilerOptions[key] !== undefined && key !== "allowJs") { 122 | jsConfig.compilerOptions![key] = tsConfig.compilerOptions[key]; 123 | } 124 | } 125 | } 126 | 127 | return jsConfig; 128 | } 129 | 130 | export async function transpileProject(projectDir: string) { 131 | const entries = await glob("**/*.+(ts|tsx)", { 132 | absolute: true, 133 | cwd: projectDir, 134 | }); 135 | 136 | const formatConfig = await resolveFormatConfig(); 137 | 138 | for (const entry of entries) { 139 | if (entry.endsWith(".d.ts")) { 140 | await fs.rm(entry); 141 | continue; 142 | } 143 | 144 | const tsx = await fs.readFile(entry, "utf8"); 145 | const mjs = format(transpileFile(tsx), formatConfig); 146 | 147 | await fs.rm(entry); 148 | await fs.writeFile(entry.replace(/\.ts(x?)$/, ".js$1"), mjs, "utf8"); 149 | } 150 | 151 | for (const [src, target] of [ 152 | ["tsconfig.json", "jsconfig.json"], 153 | ["tsconfig.node.json", "jsconfig.node.json"], 154 | ]) { 155 | // Transpile tsconfig.json to jsconfig.json 156 | try { 157 | const tsConfigPath = path.join(projectDir, src); 158 | const tsConfigWithComments = await fs.readFile(tsConfigPath, "utf8"); 159 | const jsConfig = convertConfigToJS( 160 | JSON.parse(tsConfigWithComments.replace(/^\s*\/\/.*$/gm, "")) 161 | ); 162 | 163 | await fs.rm(tsConfigPath); 164 | await fs.writeFile( 165 | path.join(projectDir, target), 166 | JSON.stringify(jsConfig, null, 2), 167 | "utf8" 168 | ); 169 | } catch (error) { 170 | console.log( 171 | "Could not transpile tsconfig.json:\n" + (error as Error).stack 172 | ); 173 | } 174 | } 175 | 176 | // Remove some TS dependencies 177 | try { 178 | const pkgJson = JSON.parse( 179 | await fs.readFile(path.join(projectDir, "package.json"), "utf8") 180 | ); 181 | 182 | for (const key of Object.keys(pkgJson.devDependencies)) { 183 | if (key.startsWith("@types/")) { 184 | delete pkgJson.devDependencies[key]; 185 | } 186 | } 187 | 188 | await fs.writeFile( 189 | path.join(projectDir, "package.json"), 190 | JSON.stringify(pkgJson, null, 2) 191 | ); 192 | } catch (error) { 193 | console.log( 194 | "Could not remove TS dependencies from package.json:\n" + 195 | (error as Error).stack 196 | ); 197 | } 198 | } 199 | 200 | async function generateJsTemplate(templateBasename: string) { 201 | const tsTemplate = `${templateBasename}-ts`; 202 | const jsTemplate = `${templateBasename}-js`; 203 | if (!existsSync(tsTemplate)) { 204 | throw new Error(`Template ${tsTemplate} does not exist`); 205 | } 206 | if (existsSync(jsTemplate)) { 207 | await fs.rm(jsTemplate, { recursive: true, force: true }); 208 | } 209 | await fs.cp(tsTemplate, jsTemplate, { recursive: true }); 210 | await fs.rm(path.join(jsTemplate, "node_modules"), { 211 | recursive: true, 212 | force: true, 213 | }); 214 | await fs.rm(path.join(jsTemplate, "dist"), { recursive: true, force: true }); 215 | await transpileProject(jsTemplate); 216 | } 217 | 218 | for (const framework of ["react", "preact"]) { 219 | const baseDir = path.join(__dirname, "..", "templates", framework); 220 | generateJsTemplate(baseDir); 221 | } 222 | -------------------------------------------------------------------------------- /scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "impala-scripts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@types/node": "^18.15.7", 15 | "@types/prettier": "^2.7.2", 16 | "fast-glob": "^3.2.12", 17 | "prettier": "^2.8.7", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^4.9.3" 20 | } 21 | } -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "NodeNext", /* Specify what module code is generated. */ 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "resolveJsonModule": true, /* Enable importing .json files. */ 36 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 37 | /* JavaScript Support */ 38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 41 | /* Emit */ 42 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 47 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 48 | // "removeComments": true, /* Disable emitting comments. */ 49 | // "noEmit": true, /* Disable emitting files from a compilation. */ 50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 58 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 59 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 62 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 64 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 65 | /* Interop Constraints */ 66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 68 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 70 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 71 | /* Type Checking */ 72 | "strict": true, /* Enable all strict type-checking options. */ 73 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 74 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 75 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 76 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 77 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 78 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 79 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 80 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 81 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 82 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 83 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 84 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 85 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 86 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 87 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 88 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 89 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 90 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 91 | /* Completeness */ 92 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 93 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 94 | } 95 | } -------------------------------------------------------------------------------- /templates/preact-js/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /templates/preact-js/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | 7 | Thanks for trying the pre-alpha of Impala. To get started, run the dev server: 8 | 9 | ```bash 10 | npm run dev 11 | ``` 12 | 13 | When you're ready to build for production, run: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | See [the GitHub repo](https://github.com/ascorbic/impala) for more help. 20 | -------------------------------------------------------------------------------- /templates/preact-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/preact-js/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "strict": true, 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext" 10 | ], 11 | "esModuleInterop": false, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "target": "ESNext", 17 | "module": "ESNext", 18 | "moduleResolution": "Node", 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": [ 22 | "src" 23 | ] 24 | } -------------------------------------------------------------------------------- /templates/preact-js/jsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "strict": true, 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ES2022" 10 | ], 11 | "esModuleInterop": true, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": [ 21 | "vite.config.js" 22 | ] 23 | } -------------------------------------------------------------------------------- /templates/preact-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demo/impala-preact-js", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "impala dev", 8 | "build:server": "vite build --ssr", 9 | "build:client": "vite build", 10 | "build:prerender": "impala prerender", 11 | "build": "npm run build:client && npm run build:server && npm run build:prerender", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@impalajs/core": "latest", 16 | "@impalajs/preact": "latest", 17 | "preact": "^10.13.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.0.0-0", 21 | "@preact/preset-vite": "^2.5.0", 22 | "typescript": "^4.9.3", 23 | "vite": "^6.0.3" 24 | } 25 | } -------------------------------------------------------------------------------- /templates/preact-js/src/App.css: -------------------------------------------------------------------------------- 1 | #__impala { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /templates/preact-js/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Head } from "@impalajs/preact/head"; 3 | 4 | export const App = ({ children, title }) => { 5 | return ( 6 | <> 7 | 8 | {title} 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /templates/preact-js/src/assets/impala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic/impala/74a4ab04744e8a539a3a7ec1225753c36ac040df/templates/preact-js/src/assets/impala.png -------------------------------------------------------------------------------- /templates/preact-js/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import { clientBootstrap } from "@impalajs/preact/client"; 2 | 3 | const modules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 4 | 5 | clientBootstrap(modules); 6 | -------------------------------------------------------------------------------- /templates/preact-js/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | export { render } from "@impalajs/preact"; 2 | export const routeModules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 3 | export const dataModules = import.meta.glob("./routes/**/*.data.{ts,js}"); 4 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/hello.data.js: -------------------------------------------------------------------------------- 1 | export function getRouteData() { 2 | return { 3 | msg: "hello world", 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/hello.jsx: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | 3 | export default function Hello({ path, routeData }) { 4 | return ( 5 | 6 |
7 | <> 8 | {routeData?.msg} {path}! 9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light; 7 | color: #213547; 8 | background-color: #ffffff; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #747bff; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid #999; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #f9f9f9; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "preact/hooks"; 2 | import { App } from "../App"; 3 | import logo from "../assets/impala.png"; 4 | import "./index.css"; 5 | 6 | export default function Hello({ path }) { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 | 11 |
12 |
13 | Impala Logo 14 |
15 |

Impala

16 |
17 | 20 |

21 | Edit src/routes/index.tsx and save to test HMR 22 |

23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/world/[id].data.js: -------------------------------------------------------------------------------- 1 | // Use this function to define pages to build, and load data for each page. 2 | export async function getStaticPaths() { 3 | return { 4 | paths: [ 5 | { params: { id: "1" }, data: { title: "One", description: "Page one" } }, 6 | { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, 7 | { 8 | params: { id: "3" }, 9 | data: { title: "Three", description: "Page three" }, 10 | }, 11 | ], 12 | }; 13 | } 14 | 15 | // This data is shared by all pages in the route. 16 | 17 | export function getRouteData() { 18 | return { 19 | msg: "hello world", 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /templates/preact-js/src/routes/world/[id].jsx: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { Head } from "@impalajs/preact/head"; 3 | 4 | export default function Hello({ path, params, data }) { 5 | return ( 6 | 7 | 8 | {/* You can also set a title tag in here if you prefer */} 9 | 10 | 11 |
12 | Hello {path} {params.id}! 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /templates/preact-js/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import preact from "@preact/preset-vite"; 3 | import impala from "@impalajs/core/plugin"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [preact(), impala()], 8 | }); 9 | -------------------------------------------------------------------------------- /templates/preact-ts/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /templates/preact-ts/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | 7 | Thanks for trying the pre-alpha of Impala. To get started, run the dev server: 8 | 9 | ```bash 10 | npm run dev 11 | ``` 12 | 13 | When you're ready to build for production, run: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | See [the GitHub repo](https://github.com/ascorbic/impala) for more help. 20 | -------------------------------------------------------------------------------- /templates/preact-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/preact-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demo/impala-preact-ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "impala dev", 8 | "build:server": "vite build --ssr", 9 | "build:client": "vite build", 10 | "build:prerender": "impala prerender", 11 | "build": "npm run build:client && npm run build:server && npm run build:prerender", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@impalajs/core": "latest", 16 | "@impalajs/preact": "latest", 17 | "preact": "^10.13.0" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.0.0-0", 21 | "@preact/preset-vite": "^2.5.0", 22 | "typescript": "^4.9.3", 23 | "vite": "^6.0.3" 24 | } 25 | } -------------------------------------------------------------------------------- /templates/preact-ts/src/App.css: -------------------------------------------------------------------------------- 1 | #__impala { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /templates/preact-ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Head } from "@impalajs/preact/head"; 3 | import { FunctionComponent } from "preact"; 4 | 5 | interface AppProps { 6 | title: string; 7 | } 8 | 9 | export const App: FunctionComponent = ({ children, title }) => { 10 | return ( 11 | <> 12 | 13 | {title} 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /templates/preact-ts/src/assets/impala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic/impala/74a4ab04744e8a539a3a7ec1225753c36ac040df/templates/preact-ts/src/assets/impala.png -------------------------------------------------------------------------------- /templates/preact-ts/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import { clientBootstrap, RouteModule } from "@impalajs/preact/client"; 2 | 3 | const modules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 4 | 5 | clientBootstrap(modules); 6 | -------------------------------------------------------------------------------- /templates/preact-ts/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import type { RouteModule, DataModule } from "@impalajs/preact"; 2 | export { render } from "@impalajs/preact"; 3 | export const routeModules = import.meta.glob( 4 | "./routes/**/*.{tsx,jsx}" 5 | ); 6 | export const dataModules = import.meta.glob( 7 | "./routes/**/*.data.{ts,js}" 8 | ); 9 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/hello.data.ts: -------------------------------------------------------------------------------- 1 | export function getRouteData() { 2 | return { 3 | msg: "hello world", 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/hello.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import type { StaticRouteProps } from "@impalajs/core"; 3 | 4 | export default function Hello({ 5 | path, 6 | routeData, 7 | }: StaticRouteProps) { 8 | return ( 9 | 10 |
11 | <> 12 | {routeData?.msg} {path}! 13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light; 7 | color: #213547; 8 | background-color: #ffffff; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #747bff; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid #999; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #f9f9f9; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { StaticRouteProps } from "@impalajs/core"; 2 | import { useState } from "preact/hooks"; 3 | import { App } from "../App"; 4 | import logo from "../assets/impala.png"; 5 | import "./index.css"; 6 | 7 | export default function Hello({ path }: StaticRouteProps) { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | 12 |
13 |
14 | Impala Logo 15 |
16 |

Impala

17 |
18 | 21 |

22 | Edit src/routes/index.tsx and save to test HMR 23 |

24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/world/[id].data.ts: -------------------------------------------------------------------------------- 1 | // Use this function to define pages to build, and load data for each page. 2 | export async function getStaticPaths() { 3 | return { 4 | paths: [ 5 | { params: { id: "1" }, data: { title: "One", description: "Page one" } }, 6 | { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, 7 | { 8 | params: { id: "3" }, 9 | data: { title: "Three", description: "Page three" }, 10 | }, 11 | ], 12 | }; 13 | } 14 | 15 | // This data is shared by all pages in the route. 16 | 17 | export function getRouteData() { 18 | return { 19 | msg: "hello world", 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /templates/preact-ts/src/routes/world/[id].tsx: -------------------------------------------------------------------------------- 1 | import { DynamicRouteProps } from "@impalajs/core"; 2 | import { App } from "../../App"; 3 | import { Head } from "@impalajs/preact/head"; 4 | 5 | export default function Hello({ 6 | path, 7 | params, 8 | data, 9 | }: DynamicRouteProps) { 10 | return ( 11 | 12 | 13 | {/* You can also set a title tag in here if you prefer */} 14 | 15 | 16 |
17 | Hello {path} {params.id}! 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /templates/preact-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /templates/preact-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 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 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "preact" 19 | }, 20 | "include": ["src"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /templates/preact-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /templates/preact-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import preact from "@preact/preset-vite"; 3 | import impala from "@impalajs/core/plugin"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [preact(), impala()], 8 | }); 9 | -------------------------------------------------------------------------------- /templates/react-js/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | 7 | Thanks for trying the pre-alpha of Impala. To get started, run the dev server: 8 | 9 | ```bash 10 | npm run dev 11 | ``` 12 | 13 | When you're ready to build for production, run: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | See [the GitHub repo](https://github.com/ascorbic/impala) for more help. 20 | -------------------------------------------------------------------------------- /templates/react-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/react-js/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "strict": true, 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext" 10 | ], 11 | "esModuleInterop": false, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "target": "ESNext", 17 | "module": "ESNext", 18 | "moduleResolution": "Node", 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "include": [ 22 | "src" 23 | ] 24 | } -------------------------------------------------------------------------------- /templates/react-js/jsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "strict": true, 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ES2022" 10 | ], 11 | "esModuleInterop": true, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "noEmit": true, 15 | "resolveJsonModule": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "allowSyntheticDefaultImports": true 19 | }, 20 | "include": [ 21 | "vite.config.js", 22 | "vite-plugin" 23 | ] 24 | } -------------------------------------------------------------------------------- /templates/react-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demo/impala-react-js", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "impala dev", 8 | "build:server": "vite build --ssr", 9 | "build:client": "vite build", 10 | "build:prerender": "impala prerender", 11 | "build": "npm run build:client && npm run build:server && npm run build:prerender", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@impalajs/core": "latest", 16 | "@impalajs/react": "latest", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@vitejs/plugin-react": "^3.1.0", 22 | "typescript": "^4.9.3", 23 | "vite": "^6.0.3" 24 | } 25 | } -------------------------------------------------------------------------------- /templates/react-js/src/App.css: -------------------------------------------------------------------------------- 1 | #__impala { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /templates/react-js/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { Head } from "@impalajs/react/head"; 4 | 5 | export const App = ({ children, title }) => { 6 | return ( 7 | <> 8 | 9 | {title} 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /templates/react-js/src/assets/impala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic/impala/74a4ab04744e8a539a3a7ec1225753c36ac040df/templates/react-js/src/assets/impala.png -------------------------------------------------------------------------------- /templates/react-js/src/entry-client.jsx: -------------------------------------------------------------------------------- 1 | import { clientBootstrap } from "@impalajs/react/client"; 2 | 3 | const modules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 4 | 5 | clientBootstrap(modules); 6 | -------------------------------------------------------------------------------- /templates/react-js/src/entry-server.jsx: -------------------------------------------------------------------------------- 1 | export { render } from "@impalajs/react"; 2 | export const routeModules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 3 | export const dataModules = import.meta.glob("./routes/**/*.data.{ts,js}"); 4 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/hello.data.js: -------------------------------------------------------------------------------- 1 | export function getRouteData() { 2 | return { 3 | msg: "hello world", 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/hello.jsx: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | 3 | export default function Hello({ path, routeData }) { 4 | return ( 5 | 6 |
7 | <> 8 | {routeData?.msg} {path}! 9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light; 7 | color: #213547; 8 | background-color: #ffffff; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #747bff; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid #999; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #f9f9f9; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { App } from "../App"; 3 | import logo from "../assets/impala.png"; 4 | import "./index.css"; 5 | 6 | export default function Hello({ path }) { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 | 11 |
12 |
13 | Impala Logo 14 |
15 |

Impala

16 |
17 | 20 |

21 | Edit src/routes/index.tsx and save to test HMR 22 |

23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/world/[id].data.js: -------------------------------------------------------------------------------- 1 | // Use this function to define pages to build, and load data for each page. 2 | export async function getStaticPaths() { 3 | return { 4 | paths: [ 5 | { params: { id: "1" }, data: { title: "One", description: "Page one" } }, 6 | { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, 7 | { 8 | params: { id: "3" }, 9 | data: { title: "Three", description: "Page three" }, 10 | }, 11 | ], 12 | }; 13 | } 14 | 15 | // This data is shared by all pages in the route. 16 | 17 | export function getRouteData() { 18 | return { 19 | msg: "hello world", 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /templates/react-js/src/routes/world/[id].jsx: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { Head } from "@impalajs/react/head"; 3 | 4 | export default function Hello({ path, params, data }) { 5 | return ( 6 | 7 | 8 | {/* You can also set a title tag in here if you prefer */} 9 | 10 | 11 |
12 | Hello {path} {params.id}! 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /templates/react-js/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import impala from "@impalajs/core/plugin"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), impala()], 7 | }); 8 | -------------------------------------------------------------------------------- /templates/react-ts/README.md: -------------------------------------------------------------------------------- 1 | # Impala 2 | 3 |

4 | 5 |

6 | 7 | Thanks for trying the pre-alpha of Impala. To get started, run the dev server: 8 | 9 | ```bash 10 | npm run dev 11 | ``` 12 | 13 | When you're ready to build for production, run: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | See [the GitHub repo](https://github.com/ascorbic/impala) for more help. 20 | -------------------------------------------------------------------------------- /templates/react-ts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/react-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@demo/impala-react-ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "impala dev", 8 | "build:server": "vite build --ssr", 9 | "build:client": "vite build", 10 | "build:prerender": "impala prerender", 11 | "build": "npm run build:client && npm run build:server && npm run build:prerender", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@impalajs/core": "latest", 16 | "@impalajs/react": "latest", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^18.15.7", 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@vitejs/plugin-react": "^3.1.0", 25 | "typescript": "^4.9.3", 26 | "vite": "^6.0.3" 27 | } 28 | } -------------------------------------------------------------------------------- /templates/react-ts/src/App.css: -------------------------------------------------------------------------------- 1 | #__impala { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /templates/react-ts/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import { Head } from "@impalajs/react/head"; 4 | 5 | interface AppProps { 6 | title: string; 7 | } 8 | 9 | export const App: React.FC> = ({ 10 | children, 11 | title, 12 | }) => { 13 | return ( 14 | <> 15 | 16 | {title} 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /templates/react-ts/src/assets/impala.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ascorbic/impala/74a4ab04744e8a539a3a7ec1225753c36ac040df/templates/react-ts/src/assets/impala.png -------------------------------------------------------------------------------- /templates/react-ts/src/entry-client.tsx: -------------------------------------------------------------------------------- 1 | import { clientBootstrap, RouteModule } from "@impalajs/react/client"; 2 | 3 | const modules = import.meta.glob("./routes/**/*.{tsx,jsx}"); 4 | 5 | clientBootstrap(modules); 6 | -------------------------------------------------------------------------------- /templates/react-ts/src/entry-server.tsx: -------------------------------------------------------------------------------- 1 | import type { RouteModule, DataModule } from "@impalajs/react"; 2 | export { render } from "@impalajs/react"; 3 | export const routeModules = import.meta.glob( 4 | "./routes/**/*.{tsx,jsx}" 5 | ); 6 | export const dataModules = import.meta.glob( 7 | "./routes/**/*.data.{ts,js}" 8 | ); 9 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/hello.data.ts: -------------------------------------------------------------------------------- 1 | export function getRouteData() { 2 | return { 3 | msg: "hello world", 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/hello.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import type { StaticRouteProps } from "@impalajs/core"; 3 | 4 | export default function Hello({ 5 | path, 6 | routeData, 7 | }: StaticRouteProps) { 8 | return ( 9 | 10 |
11 | <> 12 | {routeData?.msg} {path}! 13 | 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light; 7 | color: #213547; 8 | background-color: #ffffff; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #747bff; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid #999; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #f9f9f9; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import type { StaticRouteProps } from "@impalajs/core"; 2 | import { useState } from "react"; 3 | import { App } from "../App"; 4 | import logo from "../assets/impala.png"; 5 | import "./index.css"; 6 | 7 | export default function Hello({ path }: StaticRouteProps) { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | 12 |
13 |
14 | Impala Logo 15 |
16 |

Impala

17 |
18 | 21 |

22 | Edit src/routes/index.tsx and save to test HMR 23 |

24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/world/[id].data.ts: -------------------------------------------------------------------------------- 1 | // Use this function to define pages to build, and load data for each page. 2 | export async function getStaticPaths() { 3 | return { 4 | paths: [ 5 | { params: { id: "1" }, data: { title: "One", description: "Page one" } }, 6 | { params: { id: "2" }, data: { title: "Two", description: "Page two" } }, 7 | { 8 | params: { id: "3" }, 9 | data: { title: "Three", description: "Page three" }, 10 | }, 11 | ], 12 | }; 13 | } 14 | 15 | // This data is shared by all pages in the route. 16 | 17 | export function getRouteData() { 18 | return { 19 | msg: "hello world", 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /templates/react-ts/src/routes/world/[id].tsx: -------------------------------------------------------------------------------- 1 | import { DynamicRouteProps } from "@impalajs/core"; 2 | import { App } from "../../App"; 3 | import { Head } from "@impalajs/react/head"; 4 | 5 | export default function Hello({ 6 | path, 7 | params, 8 | data, 9 | }: DynamicRouteProps) { 10 | return ( 11 | 12 | 13 | {/* You can also set a title tag in here if you prefer */} 14 | 15 | 16 |
17 | Hello {path} {params.id}! 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /templates/react-ts/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /templates/react-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": false, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ], 26 | "references": [ 27 | { 28 | "path": "./tsconfig.node.json" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /templates/react-ts/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts", 10 | "vite-plugin" 11 | ] 12 | } -------------------------------------------------------------------------------- /templates/react-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import impala from "@impalajs/core/plugin"; 4 | 5 | export default defineConfig({ 6 | plugins: [react(), impala()], 7 | }); 8 | --------------------------------------------------------------------------------