├── .github ├── FUNDING.yml └── workflows │ ├── bench.yml │ ├── build.yml │ └── merge.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tailwind.json ├── README.md ├── bench └── run.ts ├── components ├── button.tsx ├── footer.tsx ├── header.tsx ├── icons │ ├── CategoryIcons.tsx │ ├── admin-svg.tsx │ ├── ads-svg.tsx │ ├── angle-left-svg.tsx │ ├── arrow-down.tsx │ ├── arrow-svg.tsx │ ├── bolt.tsx │ ├── deno-svg.tsx │ ├── github-svg.tsx │ ├── info-svg.tsx │ ├── preact-svg.tsx │ ├── repeat-svg.tsx │ ├── rocket-svg.tsx │ ├── sales-svg.tsx │ ├── scale-svg.tsx │ ├── seo-svg.tsx │ ├── setting-svg.tsx │ ├── tailwind-svg.tsx │ ├── ts-svg.tsx │ ├── user-account-svg.tsx │ ├── user-group-svg.tsx │ ├── ux-svg.tsx │ ├── warehouse-svg.tsx │ └── www-svg.tsx ├── inline-nav.tsx ├── project-box.tsx └── search.tsx ├── core ├── build │ ├── build.test.ts │ ├── build.ts │ ├── deps.ts │ └── esbuildMod.ts ├── map │ ├── mod.test.ts │ └── mod.ts ├── render │ ├── render.test.tsx │ └── render.ts ├── server │ ├── deps.ts │ ├── mod.test.tsx │ ├── mod.ts │ └── types.ts └── utils │ ├── kv.ts │ ├── queue.test.ts │ └── queue.ts ├── deno.json ├── docs ├── app-middleware.md ├── benchmarks.md ├── deploy.md ├── fn-component.md ├── group.md ├── hello-context.md ├── hello.md ├── json.md ├── kv.md ├── markdown.md ├── mongo.md ├── mysql.md ├── oauth.md ├── postgres.md ├── redis.md ├── route-middleware.md ├── route.md ├── ssr.md ├── start.md ├── static.md ├── store.md ├── structure.md ├── tailwind.md ├── tsx-component.md ├── tsx.md ├── url-params.md └── url-query.md ├── examples ├── app_middleware.ts ├── ctx_json.ts ├── ctx_jsx.tsx ├── ctx_string.ts ├── deno.ts ├── deno_kv.ts ├── deno_mongo.ts ├── deno_mysql.ts ├── deno_postgres.ts ├── deno_redis.ts ├── deno_sqlite.ts ├── group.ts ├── markdown_middleware.ts ├── oauth.ts ├── params_query.ts ├── raw_json.ts ├── raw_string.ts ├── route_middleware.ts ├── server_rendering.tsx ├── static_file_image.ts ├── static_file_string.ts └── string_response.ts ├── hooks └── useTypingAnimation.ts ├── middleware ├── github │ └── mod.ts ├── markdown │ ├── deps.ts │ └── mod.tsx └── tailwind │ ├── mod.ts │ └── types.ts ├── mod.ts ├── modules ├── app │ ├── main.text │ └── main.ts ├── auth │ └── mod.tsx ├── blog │ ├── blog.json │ ├── blog.layout.tsx │ └── mod.ts ├── docs │ ├── docs.json │ ├── docs.layout.tsx │ └── mod.ts ├── group │ ├── group.service.test.ts │ ├── group.service.ts │ └── group.type.ts ├── home │ ├── header.tsx │ ├── home.handler.ts │ ├── home.layout.tsx │ ├── home.page.tsx │ ├── home.service.ts │ ├── mod.ts │ ├── post.handler.ts │ └── post.page.tsx ├── hook │ ├── fetch.ts │ └── health.ts ├── markdown │ └── mod.tsx ├── store │ └── mod.ts ├── toc │ ├── toc.layout.tsx │ └── toc.page.tsx ├── types │ └── mod.ts ├── user │ ├── user.service.test.ts │ ├── user.service.ts │ └── user.type.ts ├── wait │ ├── mod.ts │ ├── wait.handler.ts │ ├── wait.layout.tsx │ ├── wait.page.tsx │ └── wait.service.ts └── web │ ├── app.layout.tsx │ ├── dear.page.tsx │ └── hello.page.tsx ├── post ├── channel.md ├── collaboration.md ├── errgroup.md ├── hello.md ├── infrastructure.md ├── mutex.md ├── oauth.md ├── preact_and_encrypted_props.md ├── queue.md ├── react.md ├── render_to_readable_stream.md ├── startup.md ├── store.md ├── tailwind.md └── waitgroup.md ├── static ├── bg.png ├── bolt.svg ├── channel.svg ├── collab.jpeg ├── fastro.jpeg ├── fastro.png ├── favicon.ico ├── gesits-2.jpg ├── gesits-3.webp ├── gesits.jpg ├── go.jpeg ├── icons │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── icon-192-maskable.png │ ├── icon-192.png │ ├── icon-512-maskable.png │ └── icon-512.png ├── infra.jpeg ├── init.ts ├── manifest.json ├── markdown.css ├── queue.jpeg ├── startup.jpeg ├── store.jpeg ├── tailwind.css └── tailwind.png ├── tailwind.config.ts ├── task ├── db_dump.ts └── db_reset.ts └── utils ├── cmd.ts ├── db.ts ├── exe.ts ├── general.ts ├── load.js ├── markdown.tsx ├── octokit.ts ├── promise.ts ├── queue.test.ts ├── queue.ts ├── session.ts └── ulid.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: fastrodev 4 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: bench 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | jobs: 7 | hello_bench: 8 | name: "Hello, bench!" 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | DB_DATABASE: homestead 13 | DB_USER: root 14 | DB_PASSWORD: root 15 | 16 | permissions: 17 | id-token: write 18 | contents: write 19 | packages: write 20 | 21 | services: 22 | postgres: 23 | image: postgres 24 | env: 25 | POSTGRES_PASSWORD: postgres 26 | POSTGRES_USER: postgres 27 | POSTGRES_DB: postgres 28 | 29 | options: >- 30 | --health-cmd pg_isready 31 | --health-interval 10s 32 | --health-timeout 5s 33 | --health-retries 5 34 | ports: 35 | - 5432:5432 36 | 37 | redis: 38 | image: redis 39 | options: >- 40 | --health-cmd "redis-cli ping" 41 | --health-interval 10s 42 | --health-timeout 5s 43 | --health-retries 5 44 | ports: 45 | - 6379:6379 46 | 47 | mongodb: 48 | image: mongo:4.4.6 49 | env: 50 | MONGO_INITDB_ROOT_USERNAME: root 51 | MONGO_INITDB_ROOT_PASSWORD: example 52 | MONGO_INITDB_DATABASE: test 53 | ports: 54 | - 27017:27017 55 | options: >- 56 | --health-cmd mongo 57 | --health-interval 10s 58 | --health-timeout 5s 59 | --health-retries 5 60 | 61 | steps: 62 | - name: Checkout Repository 63 | uses: actions/checkout@v4 64 | with: 65 | persist-credentials: false 66 | fetch-depth: 0 67 | 68 | - name: Start MySQL 69 | run: | 70 | sudo /etc/init.d/mysql start 71 | mysql -e "CREATE DATABASE IF NOT EXISTS $DB_DATABASE;" -u$DB_USER -p$DB_PASSWORD 72 | 73 | - uses: denoland/setup-deno@v2 74 | with: 75 | deno-version: v2.x 76 | 77 | - name: Install Oha 78 | uses: baptiste0928/cargo-install@v1 79 | with: 80 | crate: oha 81 | version: 1.1.0 82 | 83 | - name: Generate internal benchmarks 84 | run: GITHUB_CLIENT_ID=${{ vars.GH_CLIENT_ID }} GITHUB_CLIENT_SECRET=${{ secrets.GH_CLIENT_SECRET }} deno task bench 85 | 86 | - name: Commit files 87 | run: | 88 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 89 | git config --local user.name "github-actions[bot]" 90 | git tag -d $GITHUB_REF_NAME 91 | git tag $GITHUB_REF_NAME 92 | git commit -a -m "Add changes" 93 | - name: Push changes 94 | uses: ad-m/github-push-action@master 95 | with: 96 | github_token: ${{ secrets.GITHUB_TOKEN }} 97 | force: true 98 | tags: true 99 | 100 | - name: Run Build 101 | run: GITHUB_CLIENT_ID=${{ vars.GH_CLIENT_ID }} GITHUB_CLIENT_SECRET=${{ secrets.GH_CLIENT_SECRET }} deno task build 102 | 103 | - name: Deploy to Deno Deploy 104 | uses: denoland/deployctl@v1 105 | with: 106 | project: fastro 107 | entrypoint: modules/app/main.ts 108 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | id-token: write 12 | contents: write 13 | packages: write 14 | 15 | steps: 16 | - name: Clone repository 17 | uses: actions/checkout@v4 18 | 19 | - uses: denoland/setup-deno@v2 20 | with: 21 | deno-version: v2.x 22 | 23 | - name: Run tests 24 | run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} deno task test 25 | 26 | - name: Generate report 27 | run: deno task coverage 28 | 29 | - name: Coveralls 30 | uses: coverallsapp/github-action@v2 31 | with: 32 | path-to-lcov: ./cov.lcov 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Run Build 36 | run: GITHUB_CLIENT_ID=${{ vars.GH_CLIENT_ID }} GITHUB_CLIENT_SECRET=${{ secrets.GH_CLIENT_SECRET }} deno task build 37 | 38 | - name: Deploy to Deno Deploy 39 | uses: denoland/deployctl@v1 40 | with: 41 | project: fastro 42 | entrypoint: modules/app/main.ts 43 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: merge 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | permissions: 11 | id-token: write 12 | contents: write 13 | packages: write 14 | 15 | steps: 16 | - name: Clone repository 17 | uses: actions/checkout@v4 18 | 19 | - uses: denoland/setup-deno@v2 20 | with: 21 | deno-version: v2.x 22 | 23 | - run: deno lint --unstable-kv core 24 | - run: deno lint --unstable-kv examples 25 | 26 | - name: Run tests 27 | run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} deno task test 28 | 29 | - name: Generate report 30 | run: deno task coverage 31 | 32 | - name: Coveralls 33 | uses: coverallsapp/github-action@v2 34 | with: 35 | path-to-lcov: ./cov.lcov 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cov/ 2 | cov.lcov 3 | .hydrate 4 | .DS_Store 5 | .fastro 6 | static/js 7 | *.hydrate.tsx 8 | import_map.json 9 | node_modules/ 10 | styles.css 11 | *.db 12 | m1/ 13 | m2/ 14 | .env -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.codeLens.test": true, 5 | "deno.documentPreloadLimit": 2000, 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "denoland.vscode-deno", 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "denoland.vscode-deno" 10 | }, 11 | "[typescript]": { 12 | "editor.defaultFormatter": "denoland.vscode-deno" 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "denoland.vscode-deno" 16 | }, 17 | "[javascript]": { 18 | "editor.defaultFormatter": "denoland.vscode-deno" 19 | }, 20 | "[markdown]": { 21 | "editor.defaultFormatter": "denoland.vscode-deno" 22 | }, 23 | "css.customData": [ 24 | ".vscode/tailwind.json" 25 | ], 26 | "[css]": { 27 | "editor.defaultFormatter": "vscode.css-language-features" 28 | }, 29 | "[yaml]": { 30 | "editor.defaultFormatter": "esbenp.prettier-vscode" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/tailwind.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@tailwind", 6 | "description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#tailwind" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@apply", 16 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 17 | "references": [ 18 | { 19 | "name": "Tailwind Documentation", 20 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@responsive", 26 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@screen", 36 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 41 | } 42 | ] 43 | }, 44 | { 45 | "name": "@variants", 46 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 47 | "references": [ 48 | { 49 | "name": "Tailwind Documentation", 50 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fastro 2 | 3 | [![build](https://github.com/fastrodev/fastro/actions/workflows/build.yml/badge.svg)](https://github.com/fastrodev/fastro/actions/workflows/build.yml) 4 | [![bench](https://github.com/fastrodev/fastro/actions/workflows/bench.yml/badge.svg)](https://github.com/fastrodev/fastro/actions/workflows/bench.yml) 5 | [![deno doc](https://doc.deno.land/badge.svg)](https://deno.land/x/fastro/mod.ts) 6 | [![Coverage Status](https://coveralls.io/repos/github/fastrodev/fastro/badge.svg?branch=main)](https://coveralls.io/github/fastrodev/fastro?branch=main) 7 | 8 | 9 | 10 | Full Stack Framework for Deno, TypeScript, Preact JS and Tailwind CSS 11 | 12 | With [deno near native performance](https://fastro.deno.dev/docs/benchmarks), 13 | you can: 14 | 15 | - Manage your app and routing cleanly with 16 | [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) 17 | - Leverage existing Deno objects and methods such as 18 | [Request](jsr:api?s=Request), [Headers](jsr:api?s=Headers), 19 | [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) 20 | - Access the request, the context, and the next callback before execute the 21 | handler with 22 | [app middleware](https://github.com/fastrodev/fastro/blob/main/examples/app_middleware.ts) 23 | and 24 | [route middleware](https://github.com/fastrodev/fastro/blob/main/examples/route_middleware.ts). 25 | - You can add multiple middleware at once in one route. 26 | - Get url param with URLPattern 27 | - Set the preact component props from the server side 28 | 29 | ## Create your first end point 30 | 31 | Create a `main.ts` file for deno-cli entry point. 32 | 33 | ```ts 34 | import fastro from "https://fastro.deno.dev/mod.ts"; 35 | 36 | const f = new fastro(); 37 | 38 | f.get("/", () => "Hello, World!"); 39 | 40 | await f.serve(); 41 | ``` 42 | 43 | Run the app 44 | 45 | ``` 46 | deno run -A main.ts 47 | ``` 48 | 49 | ## Simple Examples 50 | 51 | To find one that fits your use case, you can explore 52 | [the examples page](https://github.com/fastrodev/fastro/tree/main/examples). 53 | 54 | ## SSR Example 55 | 56 | And to create your first JSX SSR page, you can follow 57 | [the start page step by step](https://fastro.deno.dev/docs/start). 58 | 59 | ## Contribution 60 | 61 | Feel free to help us! 62 | 63 | Here are some issues to improving. 64 | 65 | - [Unit tests](https://github.com/fastrodev/fastro/tree/main/http) 66 | - [Middlewares](https://github.com/fastrodev/fastro/tree/main/middleware) 67 | - [Use case examples](https://github.com/fastrodev/fastro/tree/main/examples) 68 | -------------------------------------------------------------------------------- /bench/run.ts: -------------------------------------------------------------------------------- 1 | import { delay } from "jsr:@std/async@^0.224.1"; 2 | import { markdownTable } from "npm:markdown-table@3.0.2"; 3 | const time = 10; 4 | 5 | async function oha(url?: string) { 6 | const u = url ?? "http://localhost:8000"; 7 | const args = [ 8 | "-j", 9 | "--no-tui", 10 | "-z", 11 | `${time}s`, 12 | u, 13 | ]; 14 | const oh = `oha ${args.join().replaceAll(",", " ")}`; 15 | const command = new Deno.Command("oha", { args }); 16 | const { code, stdout, stderr } = await command.output(); 17 | const err = new TextDecoder().decode(stderr); 18 | if (!err && code === 0) { 19 | const output = new TextDecoder().decode(stdout); 20 | const o = JSON.parse(output); 21 | o["oha"] = oh; 22 | return o; 23 | } 24 | } 25 | 26 | async function killServer() { 27 | const l = new Deno.Command("lsof", { 28 | args: [ 29 | "-t", 30 | "-i:8000", 31 | ], 32 | stdin: "null", 33 | stdout: "piped", 34 | stderr: "piped", 35 | }); 36 | 37 | try { 38 | const o = await l.output(); 39 | if (o && o.success) { 40 | const pid = JSON.parse(new TextDecoder().decode(o.stdout)); 41 | 42 | const c = new Deno.Command("kill", { 43 | args: [ 44 | "-9", 45 | `${pid}`, 46 | ], 47 | }); 48 | 49 | await c.output(); 50 | await delay(1000); 51 | } 52 | } catch (error) { 53 | console.log(error); 54 | } 55 | } 56 | 57 | async function bench(server: string, ext: string) { 58 | await delay(1000); 59 | 60 | const d = new Deno.Command("deno", { 61 | args: [ 62 | "task", 63 | `${server}`, 64 | ], 65 | }); 66 | 67 | d.spawn(); 68 | 69 | let res; 70 | if (server === "group") { 71 | const url = "http://localhost:8000/api/user"; 72 | res = await oha(url); 73 | } else if (server === "markdown_middleware") { 74 | const url = "http://localhost:8000/blog/hello"; 75 | res = await oha(url); 76 | } else if (server === "deno_kv") { 77 | const url = "http://localhost:8000/user\?name\=john"; 78 | res = await oha(url); 79 | } else if (server === "static_file_string") { 80 | const url = "http://localhost:8000/static/tailwind.css"; 81 | res = await oha(url); 82 | } else if (server === "static_file_image") { 83 | const url = "http://localhost:8000/static/favicon.ico"; 84 | res = await oha(url); 85 | } else if (server === "params_query") { 86 | const url = "http://localhost:8000/agus\?title\=lead"; 87 | res = await oha(url); 88 | } else { 89 | res = await oha(); 90 | } 91 | 92 | await killServer(); 93 | return { 94 | ext, 95 | module: server, 96 | requestsPerSec: res.summary.requestsPerSec, 97 | oha: `\`${res.oha}\``, 98 | }; 99 | } 100 | 101 | const server: { name: string; ext: string }[] = []; 102 | 103 | for await (const f of Deno.readDir("./examples")) { 104 | const [name, ext] = f.name.split("."); 105 | server.push({ name, ext }); 106 | } 107 | 108 | const res = []; 109 | for (const f of server) { 110 | console.log(f); 111 | const r = await bench(f.name, f.ext); 112 | res.push(r); 113 | } 114 | 115 | const table = res.sort((a, b) => b.requestsPerSec - a.requestsPerSec); 116 | const max = table[0]; 117 | 118 | const t = table.map((v) => { 119 | const relative = (v.requestsPerSec / max.requestsPerSec) * 100; 120 | const m = 121 | `[${v.module}](https://github.com/fastrodev/fastro/blob/main/examples/${v.module}.${v.ext})`; 122 | return [ 123 | m, 124 | v.requestsPerSec.toFixed(0), 125 | relative.toFixed(0) + "%", 126 | v.oha, 127 | ]; 128 | }); 129 | 130 | let markdown = `--- 131 | title: Benchmarks 132 | description: This is the final output of an internal benchmark run in github action 133 | image: https://fastro.dev/fastro.png 134 | --- 135 | 136 | This is the final output of an internal benchmark run in [github action](https://github.com/fastrodev/fastro/actions) on \`${ 137 | new Date().toLocaleString() 138 | }\`. It consists of several simple applications for [specific purpose](https://github.com/fastrodev/fastro/blob/main/deno.json). Each is then accessed by the [OHA](https://github.com/hatoo/oha) within ${time}s. The results are then sorted by the fastest. 139 | 140 | You can find the benchmark script in this code: [run.ts](https://github.com/fastrodev/fastro/blob/main/bench/run.ts) 141 | 142 | ## Benchmark results 143 | 144 | `; 145 | markdown += `\n${ 146 | markdownTable([ 147 | ["module", "rps", "%", "oha cmd"], 148 | ...t, 149 | ], { align: ["l", "r", "r", "l"] }) 150 | }`; 151 | 152 | await Deno.writeTextFile("docs/benchmarks.md", markdown); 153 | -------------------------------------------------------------------------------- /components/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentChildren } from "preact"; 2 | 3 | export default function Button(props: { children: ComponentChildren }) { 4 | return ( 5 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | function TwitterSvg() { 2 | const color = "currentColor"; 3 | return ( 4 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | function LinkedInSvg() { 24 | const color = "currentColor"; 25 | return ( 26 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | function WhatsAppSvg() { 49 | const color = "currentColor"; 50 | return ( 51 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | export function Footer(props: { 70 | isDark?: boolean; 71 | }) { 72 | const textColorClass = props.isDark ? "text-gray-100" : "text-gray-700"; 73 | return ( 74 |
77 | 103 | 104 | 112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import BoltSvg from "@app/components/icons/bolt.tsx"; 2 | // import AngleLeftSvg from "@app/components/icons/angle-left-svg.tsx"; 3 | import GithubSvg from "@app/components/icons/github-svg.tsx"; 4 | // import RocketSvg from "./icons/rocket-svg.tsx"; 5 | 6 | export default function Header( 7 | props: { 8 | isLogin: boolean; 9 | avatar_url: string; 10 | html_url: string; 11 | title?: string; 12 | previous_url?: string; 13 | isDark?: boolean; 14 | }, 15 | ) { 16 | const textColorClass = props.isDark ? "text-gray-100" : "text-gray-700"; 17 | const linkTextColorClass = props.isDark ? "text-gray-100" : "text-gray-700"; // Define link text color 18 | 19 | return ( 20 |
23 |
24 | 25 |
28 | 29 |
30 |
31 | 32 | {`${props.title || "Fastro"}`} 33 | 34 |
35 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/icons/admin-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function AdminSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /components/icons/ads-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function AdsSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/angle-left-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function AngleLeftSvg() { 2 | return ( 3 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/arrow-down.tsx: -------------------------------------------------------------------------------- 1 | export default function ArrowDownSvg() { 2 | return ( 3 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/arrow-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function ArrowSvg() { 2 | return ( 3 | 9 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/icons/bolt.tsx: -------------------------------------------------------------------------------- 1 | export default function BoltSvg(props: { height?: string; width?: string }) { 2 | return ( 3 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/deno-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function DenoSvg() { 2 | return ( 3 | 9 | Deno logo 10 | 11 | 15 | 16 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/github-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function GithubSvg() { 2 | return ( 3 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/icons/info-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function Info() { 2 | return ( 3 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/repeat-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function RepeatSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/rocket-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function RocketSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/sales-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function SalesSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/scale-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function ScaleSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/icons/seo-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function SeoSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/setting-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function SettingSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/tailwind-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function TailwindSvg() { 2 | return ( 3 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/icons/ts-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function TypeScriptSvg() { 2 | return ( 3 | 10 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/user-account-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function UserAccountSvg() { 2 | return ( 3 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/user-group-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function UserGroupsSvg() { 2 | return ( 3 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/ux-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function UxSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/warehouse-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function WareHouseSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/www-svg.tsx: -------------------------------------------------------------------------------- 1 | export default function WwwSvg() { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/inline-nav.tsx: -------------------------------------------------------------------------------- 1 | import BoltSvg from "./icons/bolt.tsx"; 2 | import ArrowSvg from "@app/components/icons/arrow-svg.tsx"; 3 | 4 | export default function InlineNav( 5 | props: { title?: string; description?: string; destination?: string }, 6 | ) { 7 | const title = props.title ?? "Fastro"; 8 | const desc = props.description ?? "Home"; 9 | const dest = props.destination ?? "/"; 10 | return ( 11 |
12 | 17 |
18 | 19 | {title} 20 |
21 |
22 | 27 | {desc} 28 | 29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/project-box.tsx: -------------------------------------------------------------------------------- 1 | import { VNode } from "preact"; 2 | 3 | export default function ProjectBox( 4 | props: { children: VNode[]; active?: boolean; url?: string }, 5 | ) { 6 | const ready = props.active ? "bg-green-700 cursor-pointer" : ""; 7 | 8 | const onClickHandler = () => { 9 | if (props.url) { 10 | location.replace(props.url); 11 | } 12 | }; 13 | 14 | return ( 15 |
19 |
20 | {props.children} 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/search.tsx: -------------------------------------------------------------------------------- 1 | export default function Search(props: { placeholder?: string }) { 2 | return ( 3 |
4 | 10 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /core/build/build.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "jsr:@std/assert@^0.225.3/assert-equals"; 2 | import { build } from "./build.ts"; 3 | 4 | Deno.test( 5 | { 6 | permissions: { env: true, read: true, write: true, run: true }, 7 | name: "build", 8 | async fn() { 9 | const r = await build(""); 10 | assertEquals(r, undefined); 11 | }, 12 | sanitizeResources: false, 13 | sanitizeOps: false, 14 | sanitizeExit: false, 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /core/build/build.ts: -------------------------------------------------------------------------------- 1 | import { denoPlugins, esbuild } from "./deps.ts"; 2 | export async function build(elementName: string) { 3 | try { 4 | const cwd = Deno.cwd(); 5 | const hydrateTarget = 6 | `${cwd}/.fastro/${elementName.toLowerCase()}.hydrate.tsx`; 7 | const configPath = `${cwd}/deno.json`; 8 | const esbuildRes = await esbuild.build({ 9 | plugins: [ 10 | ...denoPlugins({ 11 | configPath, 12 | }), 13 | ], 14 | write: Deno.env.get("ENV") !== "DEVELOPMENT", 15 | entryPoints: [hydrateTarget], 16 | outfile: `static/js/${elementName.toLowerCase()}.js`, 17 | platform: "browser", 18 | target: ["chrome99", "firefox99", "safari15"], 19 | format: "esm", 20 | jsx: "automatic", 21 | jsxImportSource: "preact", 22 | jsxFactory: "h", 23 | jsxFragment: "Fragment", 24 | absWorkingDir: cwd, 25 | bundle: true, 26 | metafile: true, 27 | treeShaking: true, 28 | minify: true, 29 | minifySyntax: true, 30 | minifyWhitespace: true, 31 | }); 32 | return esbuildRes; 33 | } catch (error) { 34 | console.error(error); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/build/deps.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "npm:esbuild@0.25.3"; 2 | export { denoPlugins } from "jsr:@luca/esbuild-deno-loader@^0.11.0"; 3 | export { esbuild }; 4 | -------------------------------------------------------------------------------- /core/build/esbuildMod.ts: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, Page } from "../server/types.ts"; 2 | import { build } from "./build.ts"; 3 | 4 | export class EsbuildMod { 5 | #elementName: string; 6 | 7 | constructor(c: Page) { 8 | const fc = c.component as FunctionComponent; 9 | this.#elementName = fc.name; 10 | } 11 | 12 | build = async () => { 13 | return await build(this.#elementName); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /core/render/render.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { 3 | h, 4 | JSX, 5 | renderToString, 6 | renderToStringAsync, 7 | VNode, 8 | } from "../server/deps.ts"; 9 | import { Fastro, FunctionComponent, Page } from "../server/types.ts"; 10 | import { BUILD_ID, getDevelopment } from "../server/mod.ts"; 11 | 12 | export class Render { 13 | #server: Fastro; 14 | constructor(server: Fastro) { 15 | this.#server = server; 16 | if (getDevelopment()) { 17 | this.#handleDevelopment(); 18 | this.#addRefreshEndPoint(); 19 | } 20 | } 21 | 22 | renderJsx = (jsx: JSX.Element, headers?: Headers) => { 23 | const html = renderToString(jsx); 24 | if (!headers) { 25 | return new Response(html, { 26 | headers: new Headers(), 27 | }); 28 | } 29 | 30 | return new Response(html, { 31 | headers, 32 | }); 33 | }; 34 | 35 | #refreshJs = (refreshUrl: string, buildId: string) => { 36 | return `const es = new EventSource('${refreshUrl}'); 37 | window.addEventListener("beforeunload", (event) => { 38 | es.close(); 39 | }); 40 | es.onmessage = function(e) { 41 | if (e.data !== "${buildId}") { 42 | location.reload(); 43 | }; 44 | };`; 45 | }; 46 | #loadJs = (name: string, nonce: string) => { 47 | return `function fetchWithRetry(t){fetch(t).then(t=>t.text()).then(t=>{if("Not Found"===t)return setTimeout(()=>{location.reload()},500);const e=document.createElement("script");e.defer = true;e.type = "module";e.nonce = '${nonce}';e.textContent=t,document.body.appendChild(e)})};const origin=new URL(window.location.origin),url=origin+"js/${name}.${this.#server.getNonce()}.js";fetchWithRetry(url);`; 48 | }; 49 | 50 | #handleDevelopment = () => { 51 | this.#server.add( 52 | "GET", 53 | "/js/refresh.js", 54 | () => 55 | new Response(this.#refreshJs(`/___refresh___`, BUILD_ID), { 56 | headers: { 57 | "Content-Type": "application/javascript", 58 | }, 59 | }), 60 | ); 61 | }; 62 | 63 | #addRefreshEndPoint = () => { 64 | const refreshStream = (_req: Request) => { 65 | let timerId: number | undefined = undefined; 66 | const body = new ReadableStream({ 67 | start(controller) { 68 | controller.enqueue(`data: ${BUILD_ID}\nretry: 100\n\n`); 69 | timerId = setInterval(() => { 70 | controller.enqueue(`data: ${BUILD_ID}\n\n`); 71 | }, 500); 72 | }, 73 | cancel() { 74 | if (timerId !== undefined) { 75 | clearInterval(timerId); 76 | } 77 | }, 78 | }); 79 | return new Response(body.pipeThrough(new TextEncoderStream()), { 80 | headers: { 81 | "content-type": "text/event-stream", 82 | }, 83 | }); 84 | }; 85 | const refreshPath = `/___refresh___`; 86 | this.#server.add("GET", refreshPath, refreshStream); 87 | }; 88 | 89 | #mutate = ( 90 | layout: VNode, 91 | component: FunctionComponent, 92 | script = "", 93 | nonce: string, 94 | id?: string, 95 | ) => { 96 | const customScript = this.#loadJs(component.name.toLowerCase(), nonce) + 97 | script; 98 | 99 | const children = layout.props.children as any; 100 | const head = children[0] as any; 101 | if (head && head.type === "head") { 102 | const c = head.props.children as any; 103 | c.push(h("meta", { 104 | name: "x-request-id", 105 | content: id, 106 | })); 107 | } 108 | 109 | children.push( 110 | h("script", { 111 | defer: true, 112 | type: "module", 113 | dangerouslySetInnerHTML: { 114 | __html: customScript, 115 | }, 116 | nonce, 117 | }), 118 | ); 119 | if (getDevelopment()) { 120 | children.push( 121 | h("script", { 122 | src: `/js/refresh.js`, 123 | async: true, 124 | nonce, 125 | }), 126 | ); 127 | } 128 | return layout; 129 | }; 130 | 131 | render = async ( 132 | _key: string, 133 | p: Page, 134 | data: T, 135 | nonce: string, 136 | hdr?: Headers, 137 | ) => { 138 | const id = Date.now().toString(); 139 | this.#server.serverOptions[id] = data; 140 | 141 | const children = typeof p.component == "function" 142 | ? h(p.component as FunctionComponent, { data, nonce }) 143 | : p.component; 144 | let app = h(p.layout as FunctionComponent, { 145 | children, 146 | data, 147 | nonce, 148 | }) as any; 149 | if (app.props.children && typeof p.component == "function") { 150 | app = this.#mutate( 151 | p.layout({ children, data, nonce }), 152 | p.component, 153 | p.script, 154 | nonce, 155 | id, 156 | ); 157 | } 158 | const html = "" + await renderToStringAsync(app); 159 | const headers = hdr ? hdr : new Headers({ 160 | "content-type": "text/html", 161 | "x-request-id": id, 162 | "Content-Security-Policy": 163 | `script-src 'self' 'unsafe-inline' 'nonce-${nonce}' https: http: ; object-src 'none'; base-uri 'none';`, 164 | }); 165 | return new Response(html, { headers }); 166 | }; 167 | } 168 | -------------------------------------------------------------------------------- /core/server/deps.ts: -------------------------------------------------------------------------------- 1 | export { STATUS_CODE, STATUS_TEXT } from "jsr:@std/http@^1.0.13"; 2 | 3 | export * from "jsr:@std/media-types@^1.1.0"; 4 | export * from "jsr:@std/path@^1.0.8"; 5 | 6 | export { encodeHex } from "jsr:@std/encoding@^1.0.7/hex"; 7 | export { assert, assertEquals, assertExists } from "jsr:@std/assert@^1.0.11"; 8 | 9 | export { h } from "npm:preact@^10.26.0 "; 10 | export type { 11 | ComponentChild, 12 | ComponentChildren, 13 | JSX, 14 | VNode, 15 | } from "npm:preact@^10.26.0 "; 16 | export { 17 | renderToString, 18 | renderToStringAsync, 19 | } from "npm:preact-render-to-string@^6.5.13"; 20 | -------------------------------------------------------------------------------- /core/utils/kv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by Deno Saaskit 3 | * See: https://github.com/denoland/saaskit/blob/main/utils/db.ts 4 | */ 5 | const DENO_KV_PATH_KEY = "DENO_KV_PATH"; 6 | let path = undefined; 7 | if ( 8 | /** 9 | * Resolves to the current status of a permission. 10 | */ 11 | (await Deno.permissions.query({ name: "env", variable: DENO_KV_PATH_KEY })) 12 | .state === "granted" 13 | ) { 14 | path = Deno.env.get(DENO_KV_PATH_KEY); 15 | } 16 | 17 | /** 18 | * Create Deno.Kv Singleton 19 | */ 20 | let instance: Deno.Kv; 21 | async function getKvInstance(path?: string): Promise { 22 | if (!instance) { 23 | instance = await Deno.openKv(path); 24 | } 25 | return instance; 26 | } 27 | 28 | export const kv = await getKvInstance(path); 29 | 30 | export async function collectValues(iter: Deno.KvListIterator) { 31 | return await Array.fromAsync(iter, ({ value }) => value); 32 | } 33 | 34 | export async function reset() { 35 | const iter = kv.list({ prefix: [] }); 36 | const promises = []; 37 | for await (const res of iter) promises.push(kv.delete(res.key)); 38 | await Promise.all(promises); 39 | } 40 | -------------------------------------------------------------------------------- /core/utils/queue.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "../../core/server/deps.ts"; 2 | import { createTaskQueue } from "../../utils/queue.ts"; 3 | 4 | Deno.test("Queue: create message", async () => { 5 | const q = createTaskQueue(); 6 | const x = await q.process(() => "x"); 7 | const y = await q.process(() => 1); 8 | const z = await q.process(() => true); 9 | const o = await q.process(() => ({})); 10 | const v = await q.process(() => {}); 11 | const p = await q.process(async () => { 12 | const r = new Promise((resolve) => resolve("hello")); 13 | return await r; 14 | }); 15 | assertEquals(x, "x"); 16 | assertEquals(y, 1); 17 | assertEquals(z, true); 18 | assertEquals(o, {}); 19 | assertEquals(v, undefined); 20 | assertEquals(p, "hello"); 21 | }); 22 | -------------------------------------------------------------------------------- /core/utils/queue.ts: -------------------------------------------------------------------------------- 1 | export function createTaskQueue() { 2 | // deno-lint-ignore no-explicit-any 3 | const array: any = []; 4 | 5 | let isExecuting = false; 6 | async function exec() { 7 | while (array.length > 0) { 8 | const { fn, args, resolve } = array.shift(); 9 | try { 10 | const result = await fn(...args); 11 | resolve(result); 12 | } catch (error) { 13 | console.error("Error executing function:", error); 14 | } 15 | } 16 | isExecuting = false; 17 | } 18 | 19 | // deno-lint-ignore no-explicit-any 20 | function process( 21 | fn: (...args: T) => Promise | R, 22 | ...args: T 23 | ): Promise { 24 | return new Promise((resolve) => { 25 | array.push({ fn, args, resolve }); 26 | if (!isExecuting) { 27 | isExecuting = true; 28 | exec(); 29 | } 30 | }); 31 | } 32 | 33 | return { process }; 34 | } 35 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "lint": { 4 | "rules": { 5 | "tags": [ 6 | "recommended" 7 | ] 8 | } 9 | }, 10 | "exports": { 11 | ".": "./mod.ts" 12 | }, 13 | "imports": { 14 | "@app/": "./", 15 | "preact": "npm:preact@^10.26.0", 16 | "@preact/signals": "npm:@preact/signals@^2.0.1", 17 | "preact-render-to-string": "npm:preact-render-to-string@^6.5.13", 18 | "preact/jsx-runtime": "npm:preact@10.26.0/jsx-runtime" 19 | }, 20 | "compilerOptions": { 21 | "jsx": "react-jsx", 22 | "jsxImportSource": "preact" 23 | }, 24 | "nodeModulesDir": "auto", 25 | "tasks": { 26 | "start": "ENV=DEVELOPMENT deno run --env --unstable-kv -A --watch modules/app/main.ts --debug", 27 | "build": "deno run --env -A --unstable-kv modules/app/main.ts --build ", 28 | "prod": "deno run --env --unstable-kv -A modules/app/main.ts", 29 | "test": "rm -rf .hydrate && rm -rf cov && deno test --env --unstable-kv -A --coverage=cov && deno coverage cov", 30 | "coverage": "deno coverage cov --lcov > cov.lcov", 31 | "bench": "deno run -A bench/run.ts", 32 | "oauth": "deno run --env -A --unstable-kv examples/oauth.ts", 33 | "deno": "deno run -A examples/deno.ts", 34 | "hook": "deno run -A examples/hook.ts", 35 | "raw_json": "deno run -A examples/raw_json.ts", 36 | "raw_jsx": "deno run -A examples/raw_jsx.tsx", 37 | "raw_string": "deno run -A examples/raw_string.ts", 38 | "ctx_json": "deno run -A examples/ctx_json.ts", 39 | "ctx_jsx": "deno run -A examples/ctx_jsx.tsx", 40 | "ctx_string": "deno run -A examples/ctx_string.ts", 41 | "string_response": "deno run -A examples/string_response.ts", 42 | "deno_kv": "deno run -A --unstable-kv examples/deno_kv.ts", 43 | "deno_sqlite": "deno run -A --unstable-ffi examples/deno_sqlite.ts", 44 | "deno_postgres": "deno run -A examples/deno_postgres.ts", 45 | "deno_mysql": "deno run -A examples/deno_mysql.ts", 46 | "deno_redis": "deno run -A examples/deno_redis.ts", 47 | "deno_mongo": "deno run -A examples/deno_mongo.ts", 48 | "params_query": "deno run -A examples/params_query.ts", 49 | "static_file_string": "deno run -A examples/static_file_string.ts", 50 | "static_file_image": "deno run -A examples/static_file_image.ts", 51 | "markdown_middleware": "deno run -A --env --unstable-kv examples/markdown_middleware.ts", 52 | "group": "deno run -A examples/group.ts", 53 | "app_middleware": "deno run -A examples/app_middleware.ts", 54 | "route_middleware": "deno run -A examples/route_middleware.ts", 55 | "server_rendering": "deno run -A examples/server_rendering.tsx", 56 | "cmd": "deno run --env -A --unstable-kv utils/cmd.ts" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/app-middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Application Middleware" 3 | description: The application that use app middleware 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: url-query 6 | next: route-middleware 7 | --- 8 | 9 | ```ts 10 | import fastro, { Context, HttpRequest } from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | const m = (req: HttpRequest, ctx: Context) => { 15 | req.ok = true; 16 | ctx.msg = "hello"; 17 | ctx.getTitle = () => "oke"; 18 | return ctx.next(); 19 | }; 20 | 21 | f.use(m); 22 | 23 | f.get("/", (req: HttpRequest, ctx: Context) => { 24 | return { 25 | ok: req.ok, 26 | msg: ctx.msg, 27 | title: ctx.getTitle(), 28 | }; 29 | }); 30 | 31 | await f.serve(); 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Benchmarks 3 | description: This is the final output of an internal benchmark run in github action 4 | image: https://fastro.dev/fastro.png 5 | --- 6 | 7 | This is the final output of an internal benchmark run in [github action](https://github.com/fastrodev/fastro/actions) on `5/31/2025, 5:56:36 AM`. It consists of several simple applications for [specific purpose](https://github.com/fastrodev/fastro/blob/main/deno.json). Each is then accessed by the [OHA](https://github.com/hatoo/oha) within 10s. The results are then sorted by the fastest. 8 | 9 | You can find the benchmark script in this code: [run.ts](https://github.com/fastrodev/fastro/blob/main/bench/run.ts) 10 | 11 | ## Benchmark results 12 | 13 | 14 | | module | rps | % | oha cmd | 15 | | :--------------------------------------------------------------------------------------------------- | -----: | ---: | :----------------------------------------------------------------- | 16 | | [deno_mongo](https://github.com/fastrodev/fastro/blob/main/examples/deno_mongo.ts) | 408418 | 100% | `oha -j --no-tui -z 10s http://localhost:8000` | 17 | | [deno_mysql](https://github.com/fastrodev/fastro/blob/main/examples/deno_mysql.ts) | 404876 | 99% | `oha -j --no-tui -z 10s http://localhost:8000` | 18 | | [deno_redis](https://github.com/fastrodev/fastro/blob/main/examples/deno_redis.ts) | 399781 | 98% | `oha -j --no-tui -z 10s http://localhost:8000` | 19 | | [deno_postgres](https://github.com/fastrodev/fastro/blob/main/examples/deno_postgres.ts) | 397410 | 97% | `oha -j --no-tui -z 10s http://localhost:8000` | 20 | | [deno](https://github.com/fastrodev/fastro/blob/main/examples/deno.ts) | 68977 | 17% | `oha -j --no-tui -z 10s http://localhost:8000` | 21 | | [params_query](https://github.com/fastrodev/fastro/blob/main/examples/params_query.ts) | 58614 | 14% | `oha -j --no-tui -z 10s http://localhost:8000/agus?title=lead` | 22 | | [string_response](https://github.com/fastrodev/fastro/blob/main/examples/string_response.ts) | 56961 | 14% | `oha -j --no-tui -z 10s http://localhost:8000` | 23 | | [raw_string](https://github.com/fastrodev/fastro/blob/main/examples/raw_string.ts) | 55733 | 14% | `oha -j --no-tui -z 10s http://localhost:8000` | 24 | | [group](https://github.com/fastrodev/fastro/blob/main/examples/group.ts) | 53925 | 13% | `oha -j --no-tui -z 10s http://localhost:8000/api/user` | 25 | | [ctx_string](https://github.com/fastrodev/fastro/blob/main/examples/ctx_string.ts) | 53562 | 13% | `oha -j --no-tui -z 10s http://localhost:8000` | 26 | | [ctx_jsx](https://github.com/fastrodev/fastro/blob/main/examples/ctx_jsx.tsx) | 52889 | 13% | `oha -j --no-tui -z 10s http://localhost:8000` | 27 | | [ctx_json](https://github.com/fastrodev/fastro/blob/main/examples/ctx_json.ts) | 50523 | 12% | `oha -j --no-tui -z 10s http://localhost:8000` | 28 | | [server_rendering](https://github.com/fastrodev/fastro/blob/main/examples/server_rendering.tsx) | 48606 | 12% | `oha -j --no-tui -z 10s http://localhost:8000` | 29 | | [deno_sqlite](https://github.com/fastrodev/fastro/blob/main/examples/deno_sqlite.ts) | 47677 | 12% | `oha -j --no-tui -z 10s http://localhost:8000` | 30 | | [app_middleware](https://github.com/fastrodev/fastro/blob/main/examples/app_middleware.ts) | 38312 | 9% | `oha -j --no-tui -z 10s http://localhost:8000` | 31 | | [raw_json](https://github.com/fastrodev/fastro/blob/main/examples/raw_json.ts) | 37372 | 9% | `oha -j --no-tui -z 10s http://localhost:8000` | 32 | | [deno_kv](https://github.com/fastrodev/fastro/blob/main/examples/deno_kv.ts) | 36792 | 9% | `oha -j --no-tui -z 10s http://localhost:8000/user?name=john` | 33 | | [markdown_middleware](https://github.com/fastrodev/fastro/blob/main/examples/markdown_middleware.ts) | 29778 | 7% | `oha -j --no-tui -z 10s http://localhost:8000/blog/hello` | 34 | | [oauth](https://github.com/fastrodev/fastro/blob/main/examples/oauth.ts) | 27453 | 7% | `oha -j --no-tui -z 10s http://localhost:8000` | 35 | | [route_middleware](https://github.com/fastrodev/fastro/blob/main/examples/route_middleware.ts) | 26903 | 7% | `oha -j --no-tui -z 10s http://localhost:8000` | 36 | | [static_file_image](https://github.com/fastrodev/fastro/blob/main/examples/static_file_image.ts) | 25164 | 6% | `oha -j --no-tui -z 10s http://localhost:8000/static/favicon.ico` | 37 | | [static_file_string](https://github.com/fastrodev/fastro/blob/main/examples/static_file_string.ts) | 24267 | 6% | `oha -j --no-tui -z 10s http://localhost:8000/static/tailwind.css` | -------------------------------------------------------------------------------- /docs/deploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Deploy" 3 | description: How to deploy application 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: group 6 | next: store 7 | --- 8 | -------------------------------------------------------------------------------- /docs/fn-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Function Component" 3 | description: How to setup a page with TSX 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: tsx-component 6 | next: ssr 7 | --- 8 | 9 | This component will build JS script. 10 | 11 | ```tsx 12 | export const hello = () => { 13 | return

Hello

; 14 | }; 15 | ``` 16 | 17 | You can also pass a props there. 18 | 19 | ```tsx 20 | export const hello = (props: { name: string }) => { 21 | return

Hello {props.name}

; 22 | }; 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/group.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Grouping modules" 3 | description: How to group several function into a module 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: kv 6 | next: deploy 7 | --- 8 | -------------------------------------------------------------------------------- /docs/hello-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello World Using Context" 3 | description: The application for creating a simple route using application context 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: hello 6 | next: json 7 | --- 8 | 9 | ```ts 10 | import fastro, { Context, HttpRequest } from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get( 15 | "/", 16 | (_req: HttpRequest, ctx: Context) => { 17 | return ctx.send("Helo world", 200); 18 | }, 19 | ); 20 | 21 | await f.serve(); 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/hello.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello World" 3 | description: The application for creating a simple route 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: structure 6 | next: hello-context 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get("/", () => "Hello, World!"); 15 | 16 | await f.serve(); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/json.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello JSON" 3 | description: The application that return simple JSON 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: hello-context 6 | next: route 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get("/", () => ({ text: "Hello json" })); 15 | 16 | await f.serve(); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/kv.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Deno KV" 3 | description: Setup Deno KV 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: mongo 6 | next: group 7 | --- 8 | -------------------------------------------------------------------------------- /docs/markdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Markdown Middleware" 3 | description: The application that use markdown middleware 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: route-middleware 6 | next: tailwind 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | import markdown from "https://fastro.deno.dev/middleware/markdown/mod.tsx"; 12 | 13 | const f = new fastro(); 14 | f.use(markdown()); 15 | 16 | await f.serve(); 17 | ``` 18 | 19 | Create `post/hello.md` file. 20 | 21 | ````md 22 | --- 23 | title: "Hello" 24 | description: "Hello markdown" 25 | image: https://fastro.deno.dev/fastro.png 26 | author: Fastro 27 | date: 11/15/2023 28 | tags: ["hello", "hi"] 29 | --- 30 | 31 | # Hello, world! 32 | 33 | | Type | Value | 34 | | ---- | ----- | 35 | | x | 42 | 36 | 37 | ```js 38 | console.log("Hello, world!"); 39 | ``` 40 | ```` 41 | 42 | Default page: `http://localhost:8000/blog/hello` 43 | -------------------------------------------------------------------------------- /docs/mongo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mongo" 3 | description: Setup Mongo 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: redis 6 | next: kv 7 | --- 8 | -------------------------------------------------------------------------------- /docs/mysql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "MySQL" 3 | description: Setup MySQL 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: oauth 6 | next: postgres 7 | --- 8 | -------------------------------------------------------------------------------- /docs/oauth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Oauth" 3 | description: Setup Oauth 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: ssr 6 | next: mysql 7 | --- 8 | -------------------------------------------------------------------------------- /docs/postgres.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Postgres" 3 | description: Setup Postgres 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: mysql 6 | next: redis 7 | --- 8 | -------------------------------------------------------------------------------- /docs/redis.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Redis" 3 | description: Setup Redis 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: postgres 6 | next: mongo 7 | --- 8 | -------------------------------------------------------------------------------- /docs/route-middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Route Middleware" 3 | description: The application that use route middleware 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: route-middleware 6 | next: markdown 7 | --- 8 | 9 | ```ts 10 | import fastro, { Context, HttpRequest } from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | const m1 = (req: HttpRequest, ctx: Context) => { 15 | // console.log("middleware 1"); 16 | req.m1 = "middleware1"; 17 | return ctx.next(); 18 | }; 19 | 20 | const m2 = (_req: HttpRequest, ctx: Context) => { 21 | // console.log("middleware 2"); 22 | return ctx.next(); 23 | }; 24 | 25 | const m3 = (_req: HttpRequest, ctx: Context) => { 26 | // console.log("middleware 3"); 27 | return ctx.next(); 28 | }; 29 | 30 | const m4 = (_req: HttpRequest, ctx: Context) => { 31 | // console.log("middleware 4"); 32 | return ctx.next(); 33 | }; 34 | 35 | const handler = (req: HttpRequest) => { 36 | // `middleware1` for get 37 | // `undefined` for post 38 | return req.m1; 39 | }; 40 | 41 | f.get("/", m1, m2, m3, handler); 42 | f.post("/", m4, handler); 43 | 44 | await f.serve(); 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/route.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Routing" 3 | description: The application that return simple route 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: json 6 | next: url-params 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get("/", () => ({ text: "Hello get" })); 15 | f.post("/", () => ({ text: "Hello post" })); 16 | f.put("/", () => ({ text: "Hello put" })); 17 | f.delete("/", () => ({ text: "Hello delete" })); 18 | 19 | await f.serve(); 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Server Side Rendering" 3 | description: Setup Server Side Rendering (SSR) Page 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: fn-component 6 | next: oauth 7 | --- 8 | 9 | To create SSR (Server-Side Rendering), we have four main steps: 10 | 11 | 1. Create the TSX or function component. 12 | 2. Create a hydration file for the component and build it with esbuild. 13 | 3. Attach the built component to the HTML layout. 14 | 4. Render the final HTML layout and component from the server. 15 | 16 | We've already streamlined these steps. All you need to prepare are the 17 | component, layout, and entry point. 18 | 19 | Create component `modules/web/hello.tsx` 20 | 21 | ```tsx 22 | export const hello = () => { 23 | return

Hello

; 24 | }; 25 | ``` 26 | 27 | Create layout `modules/web/app.layout.tsx` 28 | 29 | ```tsx 30 | import { LayoutProps } from "https://fastro.deno.dev/http/server/types.ts"; 31 | 32 | export function layout( 33 | { data, children }: LayoutProps<{ title: string }>, 34 | ) { 35 | return ( 36 | 37 | 38 | {data.title} 39 | 40 | 41 | {children} 42 | 43 | 44 | ); 45 | } 46 | ``` 47 | 48 | Create entry point `main.ts` 49 | 50 | ```ts 51 | import fastro from "https://fastro.deno.dev/mod.ts"; 52 | import { layout } from "$fastro/modules/web/app.layout.tsx"; 53 | import hello from "$fastro/modules/web/hello.page.tsx"; 54 | 55 | const f = new fastro(); 56 | 57 | f.page("/", { 58 | component: hello, 59 | layout, 60 | handler: (_req, ctx) => ctx.render({ title: "Hello world" }), 61 | folder: "modules/web", 62 | }); 63 | 64 | f.serve(); 65 | ``` 66 | 67 | You can find a more complete example in 68 | [the template repository](https://github.com/fastrodev/template). 69 | -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Getting started" 3 | description: We will create a SSR web application with one page and returning very simple react component. 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: "/" 6 | next: structure 7 | --- 8 | 9 | First, make sure you have Git and Deno installed. 10 | 11 | See 12 | [the git manual](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 13 | and 14 | [the deno manual](https://docs.deno.com/runtime/manual/getting_started/installation) 15 | for details. 16 | 17 | Generate default project from command line. 18 | 19 | ```zsh 20 | deno run -A -r https://fastro.deno.dev 21 | ``` 22 | 23 | The above command will generate default folders and files that you can use for 24 | the initial project. 25 | 26 | ```zsh 27 | cd project 28 | ``` 29 | 30 | Now let's run the application 31 | 32 | ```zsh 33 | deno task start 34 | ``` 35 | 36 | If there is no problem, you will see this message on the terminal 37 | 38 | ```zsh 39 | Listening on http://localhost:8000 40 | ``` 41 | 42 | Open that link on [your browser](http://localhost:8000) or hit them via `curl` 43 | 44 | ```zsh 45 | curl http://localhost:8000 46 | ``` 47 | 48 | You can find more detailed instructions in 49 | [the source code.](https://github.com/fastrodev/template) 50 | -------------------------------------------------------------------------------- /docs/static.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Static File" 3 | description: How to setup static file 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: tailwind 6 | next: tsx 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.static("/static", { folder: "static", maxAge: 90 }); 15 | 16 | await f.serve(); 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/store.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Store 3 | description: Store 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: deploy 6 | next: benchmarks 7 | --- 8 | 9 | ```ts 10 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | // set default value for the store 15 | f.store.set("hello", "hello world"); 16 | 17 | f.post( 18 | "/", 19 | (_req: HttpRequest, ctx: Context) => { 20 | // update default value 21 | ctx.store.set("hello", "hello world v2"); 22 | return ctx.send("Helo world", 200); 23 | }, 24 | ); 25 | 26 | f.post( 27 | "/ttl", 28 | (_req: HttpRequest, ctx: Context) => { 29 | // update default value with TTL 30 | ctx.store.set("hello", "world", 1000); 31 | return ctx.send("ttl", 200); 32 | }, 33 | ); 34 | 35 | f.post( 36 | "/commit", 37 | async (_req: HttpRequest, ctx: Context) => { 38 | // save store to github 39 | await ctx.store.commit(); 40 | return ctx.send("commit", 200); 41 | }, 42 | ); 43 | 44 | f.get( 45 | "/", 46 | (_req: HttpRequest, ctx: Context) => { 47 | // get the value 48 | const res = ctx.store.get("hello"); 49 | return Response.json({ value: res }); 50 | }, 51 | ); 52 | 53 | f.post( 54 | "/destroy", 55 | async (_req: HttpRequest, ctx: Context) => { 56 | // destroy file 57 | await ctx.store.destroy(); 58 | return ctx.send("destroy", 200); 59 | }, 60 | ); 61 | 62 | await f.serve(); 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Application Structure 3 | description: flat modular architecture for improved readability. 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: start 6 | next: hello 7 | --- 8 | 9 | You can find more detailed instructions in 10 | [the source code.](https://github.com/fastrodev/template) 11 | 12 | This is the application structure generated by `tree -I 'node_modules'` command. 13 | 14 | In this initial setup, the application consists of two modules: `index`, `user` 15 | and `markdown` modules. 16 | 17 | - `index` is for SSR page module. 18 | - `user` is for API module that provide data. 19 | - `markdown` is for handling readme file. 20 | 21 | You can modify or add new one as your need. 22 | 23 | ``` 24 | . 25 | ├── components 26 | │   ├── footer.tsx 27 | │   └── header.tsx 28 | ├── deno.json 29 | ├── main.ts 30 | ├── modules 31 | │   ├── index 32 | │   │   ├── index.handler.ts 33 | │   │   ├── index.layout.tsx 34 | │   │   ├── index.mod.ts 35 | │   │   ├── index.page.tsx 36 | │   │   └── index.service.ts 37 | │   ├── markdown 38 | │   │   ├── markdown.mod.ts 39 | │   │   └── readme.layout.tsx 40 | │   └── user 41 | │   ├── user.handler.ts 42 | │   ├── user.mod.ts 43 | │   ├── user.service.ts 44 | │   └── user.types.ts 45 | ├── readme.md 46 | ├── static 47 | │   └── tailwind.css 48 | ├── tailwind.config.ts 49 | └── utils 50 | └── db.ts 51 | ``` 52 | 53 | ## Files and Folders Descriptions 54 | 55 | | Folder / File | Description | 56 | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | 57 | | deno.json | The deno configuration file. It defines how the application behave and the shortcut of deno task command. For example: `deno task start` | 58 | | tailwind.config.ts | The tailwind configuration file. See: https://tailwindcss.com/docs/configuration | 59 | | main.ts | The application entry point. Modify it to add a new module or application-level middleware. | 60 | | utils/ | The folder that contains all library needed. Put your custom helpers here. | 61 | | utils/db.ts | The files that needed to load Deno.Kv | 62 | | modules/ | The application modules. It contains folders of modules. | 63 | | modules/index/ | The index modules. It contains page, layout, handler and SSR service. | 64 | | modules/user/ | The user modules. It contains user API endpoint and service that connected to Deno.Kv | 65 | | modules/markdown/ | The markdown modules. It contains markdown layout and middleware initiation. | 66 | | *.mod.ts | The index file for a module. Modify it to add new endpoints (API), middlewares, or pages | 67 | | *.handler.ts | The handler file for a module. It handles the request from endpoints. | 68 | | *.service.ts | The service file for a module. It functions is to provide data consumed by the handler. | 69 | | *.types.ts | The types file for a module. Place all types and interfaces here. | 70 | | *.page.tsx | The page file for a module. Create your new page with this extension. | 71 | | *.layout.tsx | The layout file for a module. Wrap your page with this layout. | 72 | | components/ | The folder that contains all components | 73 | | components/header.tsx | The file for a Header component | 74 | | components/footer.tsx | The file for a Footer component | 75 | | static/ | The folder that contains all static files needed by html files | 76 | | static/tailwind.css | The CSS file that needed by tailwind css | 77 | -------------------------------------------------------------------------------- /docs/tailwind.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tailwind Middleware" 3 | description: The application that use tailwind middleware 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: markdown 6 | next: static 7 | --- 8 | 9 | ```ts 10 | import fastro from "https://fastro.deno.dev/mod.ts"; 11 | import { tailwind } from "https://fastro.deno.dev/middleware/tailwind/mod.ts"; 12 | 13 | const f = new fastro(); 14 | f.use(tailwind()); 15 | await f.serve(); 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/tsx-component.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TSX Component" 3 | description: How to setup a page with TSX 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: static 6 | next: fn-component 7 | --- 8 | 9 | This component will not build JS script. It just render the HTML. 10 | 11 | ```tsx 12 | export const hello =

Hello

; 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/tsx.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "TSX Page" 3 | description: How to setup a page with TSX 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: static 6 | next: tsx-component 7 | --- 8 | 9 | Create `hello.tsx` file: 10 | 11 | ```ts 12 | import fastro, { Context, HttpRequest } from "https://fastro.deno.dev/mod.ts"; 13 | 14 | const f = new fastro(); 15 | 16 | f.get( 17 | "/", 18 | (_req: HttpRequest, ctx: Context) => { 19 | return ctx.render(

Hello, jsx!

); 20 | }, 21 | ); 22 | 23 | await f.serve(); 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/url-params.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "URL Params" 3 | description: The application that return simple url params 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: json 6 | next: url-query 7 | --- 8 | 9 | ```ts 10 | import fastro, { HttpRequest } from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get("/:user", (req: HttpRequest) => { 15 | const data = { user: req.params?.user }; 16 | return Response.json(data); 17 | }); 18 | 19 | await f.serve(); 20 | ``` 21 | 22 | You can access with: `http://localhost:8000/agus` 23 | -------------------------------------------------------------------------------- /docs/url-query.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "URL Query" 3 | description: The application that return simple url query 4 | image: https://fastro.deno.dev/fastro.png 5 | previous: url-params 6 | next: app-middleware 7 | --- 8 | 9 | ```ts 10 | import fastro, { HttpRequest } from "https://fastro.deno.dev/mod.ts"; 11 | 12 | const f = new fastro(); 13 | 14 | f.get("/:user", (req: HttpRequest) => { 15 | const data = { user: req.params?.user, title: req.query?.title }; 16 | return Response.json(data); 17 | }); 18 | 19 | await f.serve(); 20 | ``` 21 | 22 | You can access with: `http://localhost:8000/agus?title=head` 23 | -------------------------------------------------------------------------------- /examples/app_middleware.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | const m = (req: HttpRequest, ctx: Context) => { 6 | req.ok = true; 7 | ctx.msg = "hello"; 8 | ctx.getTitle = () => "oke"; 9 | return ctx.next(); 10 | }; 11 | 12 | f.use(m); 13 | 14 | f.get("/", (req: HttpRequest, ctx: Context) => { 15 | return { 16 | ok: req.ok, 17 | msg: ctx.msg, 18 | title: ctx.getTitle(), 19 | }; 20 | }); 21 | 22 | await f.serve(); 23 | -------------------------------------------------------------------------------- /examples/ctx_json.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get( 6 | "/", 7 | (_req: HttpRequest, ctx: Context) => { 8 | return ctx.send({ value: true }, 200); 9 | }, 10 | ); 11 | 12 | await f.serve(); 13 | -------------------------------------------------------------------------------- /examples/ctx_jsx.tsx: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get( 6 | "/", 7 | (_req: HttpRequest, ctx: Context) => { 8 | return ctx.render(

Hello, jsx!

); 9 | }, 10 | ); 11 | 12 | await f.serve(); 13 | -------------------------------------------------------------------------------- /examples/ctx_string.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get( 6 | "/", 7 | (_req: HttpRequest, ctx: Context) => { 8 | return ctx.send("Helo world", 200); 9 | }, 10 | ); 11 | 12 | await f.serve(); 13 | -------------------------------------------------------------------------------- /examples/deno.ts: -------------------------------------------------------------------------------- 1 | Deno.serve(() => new Response("Hello world")); 2 | -------------------------------------------------------------------------------- /examples/deno_kv.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | const f = new fastro( 3 | { "kv": await Deno.openKv() }, 4 | ); 5 | 6 | async function createUser(kv: Deno.Kv) { 7 | const uuid = crypto.randomUUID(); 8 | await kv.set(["users", "john"], { name: "john", id: uuid }); 9 | } 10 | 11 | // Get user 12 | f.get("/user", async (req: HttpRequest, ctx: Context) => { 13 | const kv = ctx.kv; 14 | const key: string = req.query?.name ?? ""; 15 | const u = await kv.get(["users", key]); 16 | if (!u) { 17 | await createUser(kv); 18 | } 19 | return Response.json(u.value); 20 | }); 21 | 22 | await f.serve(); 23 | -------------------------------------------------------------------------------- /examples/deno_mongo.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | import { 3 | MongoClient, 4 | ObjectId, 5 | } from "https://deno.land/x/mongo@v0.32.0/mod.ts"; 6 | 7 | const client = new MongoClient(); 8 | await client.connect("mongodb://root:example@localhost:27017"); 9 | 10 | interface UserSchema { 11 | _id: ObjectId; 12 | username: string; 13 | password: string; 14 | } 15 | 16 | const db = client.database("test"); 17 | const users = db.collection("users"); 18 | 19 | const insertId = await users.insertOne({ 20 | username: "user1", 21 | password: "pass1", 22 | }); 23 | 24 | async function getData() { 25 | const user1 = await users.findOne({ _id: insertId }); 26 | return user1; 27 | } 28 | 29 | const f = new fastro(); 30 | 31 | f.get("/", async (_req: HttpRequest, ctx: Context) => { 32 | const data = await getData(); 33 | return ctx.send(data, 200); 34 | }); 35 | 36 | await f.serve(); 37 | -------------------------------------------------------------------------------- /examples/deno_mysql.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | import { Client } from "https://deno.land/x/mysql@v2.12.1/mod.ts"; 3 | 4 | const client = await new Client().connect({ 5 | hostname: "127.0.0.1", 6 | username: "root", 7 | db: "homestead", 8 | password: "root", 9 | }); 10 | 11 | // init table and data 12 | await createTable(); 13 | 14 | async function getData() { 15 | const { rows } = await client.execute(`SELECT * FROM todos WHERE id = 1`); 16 | return rows; 17 | } 18 | 19 | const f = new fastro(); 20 | 21 | f.get( 22 | "/", 23 | async (_req: HttpRequest, ctx: Context) => { 24 | const data = await getData(); 25 | return ctx.send(data, 200); 26 | }, 27 | ); 28 | 29 | await f.serve(); 30 | 31 | async function createTable() { 32 | await client.execute(` 33 | CREATE TABLE IF NOT EXISTS todos ( 34 | id SERIAL PRIMARY KEY, 35 | title TEXT NOT NULL 36 | ) 37 | `); 38 | await client.execute(`INSERT INTO todos (title) VALUES ('hello')`); 39 | } 40 | -------------------------------------------------------------------------------- /examples/deno_postgres.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | import { Client } from "https://deno.land/x/postgres@v0.17.0/mod.ts"; 3 | 4 | const client = new Client({ 5 | user: "postgres", 6 | database: "postgres", 7 | password: "postgres", 8 | hostname: "localhost", 9 | port: 5432, 10 | }); 11 | 12 | // init table and data 13 | await createTable(); 14 | 15 | // init connection 16 | await client.connect(); 17 | 18 | async function getData() { 19 | const { rows } = await client.queryObject`SELECT * FROM todos WHERE id = 1`; 20 | return rows; 21 | } 22 | 23 | const f = new fastro(); 24 | 25 | f.get( 26 | "/", 27 | async (_req: HttpRequest, ctx: Context) => { 28 | const data = await getData(); 29 | return ctx.send(data, 200); 30 | }, 31 | ); 32 | 33 | await f.serve(); 34 | 35 | async function createTable() { 36 | if (!client) throw new Error(); 37 | await client.connect(); 38 | try { 39 | await client.queryObject` 40 | CREATE TABLE IF NOT EXISTS todos ( 41 | id SERIAL PRIMARY KEY, 42 | title TEXT NOT NULL 43 | ) 44 | `; 45 | await client.queryObject`INSERT INTO todos (title) VALUES ('hello')`; 46 | } finally { 47 | await client.end(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/deno_redis.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | import { connect } from "https://deno.land/x/redis@v0.32.1/mod.ts"; 3 | 4 | const redis = await connect({ 5 | hostname: "localhost", 6 | port: 6379, 7 | }); 8 | 9 | await redis.set("hoge", "fuga"); 10 | 11 | async function getData() { 12 | return await redis.get("hoge"); 13 | } 14 | 15 | const f = new fastro(); 16 | 17 | f.get( 18 | "/", 19 | async (_req: HttpRequest, ctx: Context) => { 20 | const data = await getData(); 21 | return ctx.send(data, 200); 22 | }, 23 | ); 24 | 25 | await f.serve(); 26 | -------------------------------------------------------------------------------- /examples/deno_sqlite.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "jsr:@db/sqlite"; 2 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 3 | 4 | const db = new Database("sqlite.db"); 5 | 6 | function getData() { 7 | const [version] = db.prepare("select sqlite_version()").value<[string]>()!; 8 | return version; 9 | } 10 | 11 | const f = new fastro(); 12 | 13 | f.get("/", (_req: HttpRequest, ctx: Context) => { 14 | const data = getData(); 15 | return ctx.send(data, 200); 16 | }); 17 | 18 | await f.serve(); 19 | -------------------------------------------------------------------------------- /examples/group.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Fastro } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | const helloModule = (f: Fastro) => { 6 | return f.get("/", () => "Hello World"); 7 | }; 8 | 9 | const userModule = (f: Fastro) => { 10 | const path = `/api/user`; 11 | return f.get(path, () => "Get user") 12 | .post(path, () => "Add user") 13 | .put(path, () => "Update user") 14 | .delete(path, () => "Delete user"); 15 | }; 16 | 17 | const productModule = (f: Fastro) => { 18 | const path = `/api/product`; 19 | return f.get(path, () => "Get product") 20 | .post(path, () => "Add product") 21 | .put(path, () => "Update product") 22 | .delete(path, () => "Delete product"); 23 | }; 24 | 25 | await f.group(helloModule); 26 | await f.group(userModule); 27 | await f.group(productModule); 28 | 29 | await f.serve(); 30 | -------------------------------------------------------------------------------- /examples/markdown_middleware.ts: -------------------------------------------------------------------------------- 1 | import markdown from "@app/middleware/markdown/mod.tsx"; 2 | import fastro from "@app/mod.ts"; 3 | 4 | const f = new fastro(); 5 | 6 | // default page: 7 | // http://localhost:8000/blog/hello 8 | f.use(markdown()); 9 | 10 | await f.serve(); 11 | -------------------------------------------------------------------------------- /examples/oauth.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | import authModule from "@app/modules/auth/mod.tsx"; 3 | 4 | const f = new fastro(); 5 | f.group(authModule); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /examples/params_query.ts: -------------------------------------------------------------------------------- 1 | import fastro, { HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get("/:user", (req: HttpRequest) => { 6 | const data = { user: req.params?.user, name: req.query?.title }; 7 | return Response.json(data); 8 | }); 9 | 10 | await f.serve(); 11 | -------------------------------------------------------------------------------- /examples/raw_json.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get("/", () => ({ text: "Hello json" })); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /examples/raw_string.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get("/", () => "Hello, World!"); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /examples/route_middleware.ts: -------------------------------------------------------------------------------- 1 | import fastro, { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | const m1 = (req: HttpRequest, ctx: Context) => { 6 | // console.log("middleware 1"); 7 | req.m1 = "middleware1"; 8 | return ctx.next(); 9 | }; 10 | 11 | const m2 = (_req: HttpRequest, ctx: Context) => { 12 | // console.log("middleware 2"); 13 | return ctx.next(); 14 | }; 15 | 16 | const m3 = (_req: HttpRequest, ctx: Context) => { 17 | // console.log("middleware 3"); 18 | return ctx.next(); 19 | }; 20 | 21 | const m4 = (_req: HttpRequest, ctx: Context) => { 22 | // console.log("middleware 4"); 23 | return ctx.next(); 24 | }; 25 | 26 | const handler = (req: HttpRequest) => { 27 | // `middleware1` for get 28 | // `undefined` for post 29 | return req.m1; 30 | }; 31 | 32 | f.get("/", m1, m2, m3, handler); 33 | f.post("/", m4, handler); 34 | 35 | await f.serve(); 36 | -------------------------------------------------------------------------------- /examples/server_rendering.tsx: -------------------------------------------------------------------------------- 1 | import Server from "@app/mod.ts"; 2 | import layout from "@app/modules/web/app.layout.tsx"; 3 | import hello from "@app/modules/web/hello.page.tsx"; 4 | 5 | const s = new Server(); 6 | 7 | s.page("/", { 8 | component: hello, 9 | layout, 10 | handler: (_req, ctx) => ctx.render({ title: "Hello world" }), 11 | folder: "modules/web", 12 | }); 13 | 14 | s.serve(); 15 | -------------------------------------------------------------------------------- /examples/static_file_image.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.static("/static", { folder: "static", maxAge: 90 }); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /examples/static_file_string.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.static("/static", { folder: "static", maxAge: 90 }); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /examples/string_response.ts: -------------------------------------------------------------------------------- 1 | import fastro from "@app/mod.ts"; 2 | 3 | const f = new fastro(); 4 | 5 | f.get("/", () => new Response("Hello world")); 6 | 7 | await f.serve(); 8 | -------------------------------------------------------------------------------- /hooks/useTypingAnimation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "preact/hooks"; 2 | import { JSX } from "preact/jsx-runtime"; 3 | 4 | interface UseTypingAnimationProps { 5 | text: string | JSX.Element; 6 | shouldType?: boolean; 7 | onComplete?: () => void; 8 | minDelay?: number; 9 | maxDelay?: number; 10 | } 11 | 12 | export function useTypingAnimation({ 13 | text, 14 | shouldType = true, 15 | onComplete, 16 | minDelay = 30, 17 | maxDelay = 70, 18 | }: UseTypingAnimationProps) { 19 | const [displayText, setDisplayText] = useState(""); 20 | 21 | useEffect(() => { 22 | setDisplayText(""); 23 | if (!shouldType) return; 24 | 25 | let timeoutId: number; 26 | let currentIndex = 0; 27 | let isActive = true; 28 | 29 | const typeNextCharacter = () => { 30 | if (!isActive) return; 31 | 32 | if ((typeof text === "string") && currentIndex <= text.length) { 33 | setDisplayText(text.slice(0, currentIndex)); 34 | 35 | if (currentIndex === text.length) { 36 | if (onComplete) onComplete(); 37 | } else { 38 | currentIndex++; 39 | const randomDelay = Math.random() * (maxDelay - minDelay) + minDelay; 40 | timeoutId = setTimeout(typeNextCharacter, randomDelay); 41 | } 42 | } 43 | }; 44 | 45 | timeoutId = setTimeout(typeNextCharacter, 0); 46 | 47 | return () => { 48 | isActive = false; 49 | if (timeoutId) clearTimeout(timeoutId); 50 | }; 51 | }, [text, shouldType, minDelay, maxDelay]); 52 | 53 | return { displayText }; 54 | } 55 | -------------------------------------------------------------------------------- /middleware/github/mod.ts: -------------------------------------------------------------------------------- 1 | import { Context, HttpRequest } from "@app/mod.ts"; 2 | 3 | export default async function github(_req: HttpRequest, ctx: Context) { 4 | if ( 5 | ctx.url.pathname.endsWith(".ts") || 6 | ctx.url.pathname.endsWith(".tsx") 7 | ) { 8 | const version = ctx.url.pathname.startsWith("/v") 9 | ? "" 10 | : ctx.url.pathname.startsWith("/canary") 11 | ? "/canary" 12 | : "/main"; 13 | 14 | const path = 15 | `https://raw.githubusercontent.com/fastrodev/fastro${version}${ctx.url.pathname}`; 16 | const res = await fetch(path); 17 | const content = await res.text(); 18 | return new Response(content); 19 | } 20 | return ctx.next(); 21 | } 22 | -------------------------------------------------------------------------------- /middleware/markdown/deps.ts: -------------------------------------------------------------------------------- 1 | export { CSS, render } from "jsr:@deno/gfm@0.6"; 2 | export { extract } from "jsr:@std/front-matter/any"; 3 | export * as remarkToc from "npm:remark-toc@8.0.1"; 4 | export { remark } from "npm:remark@14.0.3"; 5 | -------------------------------------------------------------------------------- /middleware/markdown/mod.tsx: -------------------------------------------------------------------------------- 1 | import { CSS, extract, remark, render } from "./deps.ts"; 2 | import { renderToString } from "../../core/server/deps.ts"; 3 | import { Context, FunctionComponent, HttpRequest } from "../../mod.ts"; 4 | 5 | import "npm:prismjs@^1.30.0/components/prism-css.min.js"; 6 | import "npm:prismjs@^1.30.0/components/prism-jsx.min.js"; 7 | import "npm:prismjs@^1.30.0/components/prism-typescript.min.js"; 8 | import "npm:prismjs@^1.30.0/components/prism-tsx.min.js"; 9 | import "npm:prismjs@^1.30.0/components/prism-bash.min.js"; 10 | import "npm:prismjs@^1.30.0/components/prism-powershell.min.js"; 11 | import "npm:prismjs@^1.30.0/components/prism-json.min.js"; 12 | import "npm:prismjs@^1.30.0/components/prism-diff.min.js"; 13 | import "npm:prismjs@^1.30.0/components/prism-go.min.js"; 14 | 15 | function stringToJSXElement(content: string) { 16 | return
; 17 | } 18 | 19 | function readMarkdownFile(folder: string, file: string) { 20 | try { 21 | const path = folder + "/" + file + ".md"; 22 | const md = Deno.readTextFileSync(path); 23 | return md; 24 | } catch { 25 | return null; 26 | } 27 | } 28 | 29 | const record: Record = {}; 30 | export async function getMarkdownBody( 31 | req: Request, 32 | layout: FunctionComponent, 33 | folder: string, 34 | file: string, 35 | prefix: string, 36 | data?: unknown, 37 | ) { 38 | // deno-lint-ignore no-explicit-any 39 | async function g(): Promise<(any)[] | null> { 40 | const id = folder + file; 41 | if (record[id]) { 42 | // deno-lint-ignore no-explicit-any 43 | return record[id] as any[]; 44 | } 45 | const filePath = prefix ? file.replace(`/${prefix}/`, "") : file; 46 | const pathname = prefix ? `/${prefix}/${filePath}` : file; 47 | 48 | const pattern = new URLPattern({ pathname }); 49 | const passed = pattern.test(req.url); 50 | if (!passed) return null; 51 | 52 | const md = readMarkdownFile(folder, filePath); 53 | if (!md) return null; 54 | 55 | const m = extract(md); 56 | const f = await remark().process(m.body); 57 | const rendered = render(String(f)); 58 | return record[id] = [rendered, m.attrs]; 59 | } 60 | 61 | const resp = await g(); 62 | if (!resp) return null; 63 | const [r, attrs] = resp; 64 | const markdown = stringToJSXElement(r); 65 | 66 | const html = layout({ 67 | CSS, 68 | markdown, 69 | attrs, 70 | data, 71 | }); 72 | 73 | return "" + renderToString(html); 74 | } 75 | 76 | export const defaultLayout = (props: { 77 | CSS: string; 78 | markdown: string; 79 | attrs: Record; 80 | data?: unknown; 81 | }) => { 82 | return ( 83 | 84 | 85 | 86 | 87 | 90 | 94 | 95 | 96 |
102 | {props.markdown} 103 |
104 |