Compare commits
302 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 209c9fb7f7 | |||
| a562f67e8d | |||
| ef0fe5a425 | |||
| b50902d23e | |||
| 55490579ec | |||
| 957d08701f | |||
| 61e1aed35d | |||
| 2034f0ef5f | |||
| fcb92c7f0b | |||
| 28ea1ca1df | |||
| ebc1a458a9 | |||
| df92ef5296 | |||
| 253e0a8378 | |||
| a9fbe4eddd | |||
| cfccc638c6 | |||
| 9f1460cc26 | |||
| f0a4541420 | |||
| d0ce482680 | |||
| 40e2734a4e | |||
| 8552796343 | |||
| 6d72ffb876 | |||
| 65faaf0183 | |||
| 7f7c2e8d6f | |||
| 3be491baa7 | |||
| 48b974ae58 | |||
| c394898fcd | |||
| f22249f3e9 | |||
| 120cec52e9 | |||
| 535cc6b3d9 | |||
| 18a060e2bf | |||
| 7b8165cee6 | |||
| 6331eb64e6 | |||
| 3c38b551f0 | |||
| 8e761347f2 | |||
| e2d44e0e6a | |||
| 607da92c71 | |||
| ae0427604e | |||
| 2aa981c581 | |||
| 5d8ec82502 | |||
| fc9bb52c41 | |||
| 3224e4138f | |||
| 7ea28c4fd3 | |||
| acb5567f6a | |||
| cc877c7c11 | |||
| 87bdddf7cf | |||
| d939b64d64 | |||
| 3f64f71947 | |||
| aaae7db24d | |||
| 48c3934b6e | |||
| 258b4af0b1 | |||
| edfbd03f13 | |||
| 9cb0f62da7 | |||
| 9591960e31 | |||
| 132d8ebe8f | |||
| 4020073aca | |||
| 5025692220 | |||
| d04a4c4103 | |||
| 7e6dea68f4 | |||
| a046833414 | |||
| cfa59ca8bf | |||
| d470bd58f2 | |||
| ea82d2b1df | |||
| 93b44088a9 | |||
| 625c091fdc | |||
| e0c0447fb6 | |||
| b67a8e8537 | |||
| c9f1b02954 | |||
| 6b3bfca1f6 | |||
| 15e568594d | |||
| 089f41ee89 | |||
| 636c68e532 | |||
| 6d8a04dfb2 | |||
| 01d5aad8aa | |||
| 002733cd99 | |||
| d7a086dfb1 | |||
| d60c7dcd9b | |||
| 58baf8aa82 | |||
| 0a8504d9f2 | |||
| 934a83c0e2 | |||
| ea65ad042f | |||
| 3544cd2943 | |||
| e593707ce7 | |||
| 6dd0fc7410 | |||
| faf7bf29e2 | |||
| c5b5f6aefe | |||
| 8e48eec01d | |||
| e3c5cc882f | |||
| dfb1bebc6b | |||
| a40367f813 | |||
| 1a1b62698c | |||
| b36dc0b897 | |||
| a9eacf21f4 | |||
| 23d84b25be | |||
| 6c7cc62371 | |||
| 588326cc57 | |||
| ce654bf7a2 | |||
| a3bcb76105 | |||
| 4045ebc24e | |||
| 2be3710462 | |||
| fe3185c6dc | |||
| f10b813e1e | |||
| c67d4199a0 | |||
| d0a6c59c62 | |||
| 60f622542f | |||
| e7fcd64b3a | |||
| 3772e7307e | |||
| e853db164d | |||
| 509283adab | |||
| db56b7f2dc | |||
| acd663d79f | |||
| 3636268a71 | |||
| 6402f42d01 | |||
| 71ac19cbb9 | |||
| d99389a398 | |||
| 024c386af4 | |||
| d982c9c24b | |||
| 480973318d | |||
| a497937106 | |||
| 0d33150f30 | |||
| 94667b1454 | |||
| 2cbcd8ad70 | |||
| 9f40a55907 | |||
| 51e01ec142 | |||
| 63fc2b9855 | |||
| edd8a02423 | |||
| a0a81b514c | |||
| 92820804f8 | |||
| e7ea7ed1c4 | |||
| 7471c90f49 | |||
| f545a9c469 | |||
| 30073c226d | |||
| 1b1e91b6c0 | |||
| 71d7ccf27d | |||
| 3ca24ec87f | |||
| b2940d9fa8 | |||
| a7b7f0eac2 | |||
| 4859916edd | |||
| ad3e19e965 | |||
| 03dead1083 | |||
| 341fba6ca9 | |||
| 58c4890122 | |||
| af42e5ef21 | |||
| 5abff0384c | |||
| cc915560b8 | |||
| 9835a4c0d1 | |||
| 90782ae710 | |||
| 75c790dd6e | |||
| 8604858a61 | |||
| d1b6162d3a | |||
| a445644b47 | |||
| ebb4434506 | |||
| 36a53662a0 | |||
| 569d95dc9c | |||
| bc4c4e03f0 | |||
| 170c5ea098 | |||
| 3fd5f166e2 | |||
| a2285a78a1 | |||
| 1ae7f72de3 | |||
| 19782a2dbe | |||
| b755e658f5 | |||
| cc0279b964 | |||
| 099da549b5 | |||
| 92a76032eb | |||
| cd17eebf96 | |||
| 1726312d34 | |||
| 444d1479b0 | |||
| 2949ea4dfa | |||
| 8ab292b2f9 | |||
| 9d8bfd9826 | |||
| 8a6685a6d0 | |||
| 78990d3c08 | |||
| 227662489b | |||
| 5ce410f233 | |||
| 67d4e1bcac | |||
| 3e6aa540fa | |||
| b412371287 | |||
| f62fc0b2b9 | |||
| 35d8154649 | |||
| 6e794e9500 | |||
| e4d4a75b88 | |||
| bc7ffc2246 | |||
| 74e2f4e873 | |||
| d9d62a91f7 | |||
| 77b34fbb7b | |||
| d3040aa63f | |||
| 672e4b69d7 | |||
| 9cd90ba62a | |||
| b6d0bb13ef | |||
| 7401a59346 | |||
| 622ae93d68 | |||
| b4207feb7e | |||
| 332fdb70ca | |||
| b9abfb64c0 | |||
| 3ca8861d85 | |||
| 0c93aab13f | |||
| 208b49cfc8 | |||
| db0a2d9d1d | |||
| c123e93433 | |||
| 35557d296d | |||
| d0d6550d7e | |||
| 38d9be2bb0 | |||
| fcb33d208c | |||
| f831ad6c7a | |||
| 3bfed5fabc | |||
| e1ce3ba2fd | |||
| a31f048ffd | |||
| 9c707466db | |||
| 4ff899729b | |||
| a8345d3509 | |||
| 1c3d6e2d78 | |||
| dde5ae7827 | |||
| aeef19cda3 | |||
| adec05c6f8 | |||
| 2c1e8f7db8 | |||
| 2c1e8f0cc2 | |||
| 5d434aeb68 | |||
| 54e7d3a397 | |||
| 7937291281 | |||
| 777f932266 | |||
| f37cb701b3 | |||
| 6498905377 | |||
| ed6c75fe37 | |||
| f759960641 | |||
| 75e8e51464 | |||
| 75ee140525 | |||
| 8d09637a7a | |||
| 62bfd35fbd | |||
| 091c94d609 | |||
| a72656cb4b | |||
| b3ef9f0ee6 | |||
| 0e4debdb8a | |||
| 476fae0e70 | |||
| 3de9d1be66 | |||
| 00916f188d | |||
| fff5f92893 | |||
| 8b796fc1f4 | |||
| 5ac679785b | |||
| 38b6cc8c65 | |||
| abb2d658c6 | |||
| 3587c085b9 | |||
| 581a7d79b3 | |||
| e21b80a5d8 | |||
| e0fc19ecaf | |||
| 0be0ce4c15 | |||
| 792358ea9c | |||
| 26c0c2488d | |||
| 70b4b9c28f | |||
| 8b4db423b4 | |||
| 7d65ede581 | |||
| 5b11f68fbb | |||
| 0d33e6887b | |||
| 5bb620a590 | |||
| 469386388b | |||
| 8728499f90 | |||
| 2e82dde2c3 | |||
| 253dd44d0d | |||
| 821b38e269 | |||
| 753bd21c14 | |||
| 10757d4ef4 | |||
| d436637086 | |||
| 0b5679ee07 | |||
| 97b6cdfaaa | |||
| b10d01cae0 | |||
| 2262264cae | |||
| bedd23eed8 | |||
| 12a1b12bac | |||
| c141e0b843 | |||
| 4d12fd9d20 | |||
| 5322fe910a | |||
| 70a6aa1a67 | |||
| 8d2be4178b | |||
| e89f2f63f8 | |||
| f6e7ebd33e | |||
| 5ea209de0b | |||
| a4b2dd222d | |||
| c84f5d479b | |||
| 9251d445d3 | |||
| c1ff7e36a8 | |||
| 8dd8123fb1 | |||
| 7040fbfb3d | |||
| ee2abbd4e1 | |||
| 252c463300 | |||
| a10f205ccd | |||
| 7cfaf94146 | |||
| 7557df10f3 | |||
| 64c37f42c5 | |||
| 00594b7c3a | |||
| 930e7bc851 | |||
| d7d0001299 | |||
| 1c0b4bba04 | |||
| c12739d526 | |||
| 0780a6ec79 | |||
| 73e142a5d7 | |||
| d3cee9f1b2 | |||
| e6370cb7d4 | |||
| e2e6e313a5 | |||
| 2348576fa4 | |||
| 9550f8243c | |||
| 1d87a78393 | |||
| ee092194d1 | |||
| 47a02cc572 | |||
| 1849c49682 |
@@ -35,7 +35,9 @@
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/ban-types": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
@@ -47,7 +49,7 @@
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "typeParameter",
|
||||
"selector": "typeParameter",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
@@ -57,6 +59,7 @@
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
examples/** linguist-documentation
|
||||
*.css linguist-detectable=false
|
||||
*.scss linguist-detectable=false
|
||||
*.js linguist-detectable=false
|
||||
*.jsx linguist-detectable=true
|
||||
*.tsx linguist-detectable=true
|
||||
*.ts linguist-detectable=true
|
||||
*.rs linguist-detectable=true
|
||||
*.html linguist-detectable=false
|
||||
*.sh linguist-detectable=false
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behaviour
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Steps to reproduce the behavior (code, example repository)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: tuono-version
|
||||
attributes:
|
||||
label: Tuono version
|
||||
placeholder: '[e.g. 0.4.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: '[e.g. MacOS, Windows]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: browser-version
|
||||
attributes:
|
||||
label: Browser
|
||||
placeholder: '[e.g. chrome, safari]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node version
|
||||
placeholder: '[e.g. 20.0.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: rust-version
|
||||
attributes:
|
||||
label: Rust version
|
||||
placeholder: '[e.g. 1.79.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: create-version
|
||||
attributes:
|
||||
label: Crate version
|
||||
placeholder: '[e.g. 1.78.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: node-package-manager-version
|
||||
attributes:
|
||||
label: Node Package Manger version
|
||||
placeholder: '[e.g. pnpm: 9.5.0] '
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the bug here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature request]: '
|
||||
labels: ['enhancement']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Install NodeJS Dependencies
|
||||
description: This is a composite GitHub Action that sets up pnpm, node and installs the project's dependencies.
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'Explicit node version. Otherwise fallback reading `.nvmrc`. Use in conjunction with matrix'
|
||||
required: false
|
||||
registry-url:
|
||||
description: 'https://github.com/actions/setup-node?tab=readme-ov-file#usage'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js (via input)
|
||||
if: ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Node.js (via .nvmrc)
|
||||
if: ${{ !inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -0,0 +1,13 @@
|
||||
## Context & Description
|
||||
|
||||
<!--
|
||||
Thank you for your Pull Request.
|
||||
|
||||
Explain the context and why you're making that change. What is the problem
|
||||
you're trying to solve? If a new feature is being added, describe the intended
|
||||
use case that feature fulfills.
|
||||
|
||||
Bug fixes and new features should include tests.
|
||||
|
||||
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Documentation Website CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.12.3
|
||||
|
||||
- name: Build project
|
||||
working-directory: ./apps/documentation
|
||||
run: tuono build --static
|
||||
|
||||
fmt-lint-and-types:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check format and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm docs:format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm docs:lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm docs:types
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Deploy documentation website on AWS S3
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
|
||||
jobs:
|
||||
deploy-documentation:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.12.3
|
||||
|
||||
- name: Build project
|
||||
working-directory: ./apps/documentation
|
||||
run: tuono build --static
|
||||
|
||||
- name: Deploy
|
||||
uses: reggionick/s3-deploy@v4
|
||||
with:
|
||||
folder: apps/documentation/out/static
|
||||
bucket: tuono-documentation
|
||||
bucket-region: eu-west-3
|
||||
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||
invalidation: /
|
||||
delete-removed: true
|
||||
no-cache: true
|
||||
private: true
|
||||
files-to-include: '{.*/**,**}'
|
||||
@@ -7,45 +7,35 @@ jobs:
|
||||
publish-on-npm-dry-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install turbo
|
||||
run: pnpm install turbo --global
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --no-git-checks --dry-run --filter ./packages/tuono
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates-dry-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
dry-run: true
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
@@ -55,32 +45,22 @@ jobs:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install turbo
|
||||
run: pnpm install turbo --global
|
||||
- name: Clone root README.md on tuono package
|
||||
run: cp README.md packages/tuono/README.md
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --no-git-checks --filter ./packages/tuono
|
||||
run: pnpm publish -r --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -88,13 +68,16 @@ jobs:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Repo root CI
|
||||
# All workflows are triggered based on specific paths,
|
||||
# the root repository is something that is not applicable to rust nor typescript
|
||||
# so it has been moved in a standalone workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- './*'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- './*'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check format
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Test project
|
||||
run: pnpm repo:root:format
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'crates/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'crates/**'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
lint_and_fmt:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check format and lint rust crates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
build_and_test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Build and test rust crates
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||
- run: cargo build --verbose
|
||||
- run: cargo test --verbose
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Spell Checker
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
spell_checking:
|
||||
name: runner / LanguageTools
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: reviewdog/action-languagetool@v1
|
||||
with:
|
||||
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
|
||||
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
|
||||
reporter: 'github-pr-review'
|
||||
# Change reporter level if you need.
|
||||
level: 'error'
|
||||
patterns: '**/*.md **/*.txt **/*.mdx'
|
||||
#enabled_categories: 'STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END'
|
||||
disabled_categories: ''
|
||||
disabled_rules: ''
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Typescript CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'packages/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'packages/**'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check build and test
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Test project
|
||||
run: pnpm test
|
||||
|
||||
fmt-lint-and-types:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check format and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
@@ -1,12 +1,12 @@
|
||||
## Node related ignores
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
.turbo/
|
||||
|
||||
*/**/routeTree.gen.ts
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
examples/playground/
|
||||
examples/*/pnpm-lock.yaml
|
||||
|
||||
## Rust related ignores
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
dist/
|
||||
pnpm-lock.yaml
|
||||
pnpm-lock.yaml
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We, as members, contributors, and leaders, pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best, not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
Advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct that could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned with this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
An individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
valerioageno@yahoo.it.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,24 @@
|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
# How to Contribute to Tuono
|
||||
|
||||
## Contributions
|
||||
|
||||
Any feature contribution or suggestion is strongly appreciated.
|
||||
Since the current project size, there isn't yet a defined way to start a discussion. Consider [opening a new issue](https://github.com/Valerioageno/tuono/issues/new/choose)
|
||||
or to reach me using my email address, [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on Twitter (X); DM @valerioageno.
|
||||
|
||||
## Bugs
|
||||
|
||||
**Did you find a bug?**
|
||||
|
||||
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Valerioageno/tuono/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
|
||||
|
||||
**Did you write a patch that fixes a bug?**
|
||||
|
||||
- Open a new GitHub pull request with the patch.
|
||||
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number, if applicable.
|
||||
- The pull requests must pass all the CI pipelines
|
||||
@@ -3,7 +3,12 @@ resolver = "2"
|
||||
members = [
|
||||
"crates/tuono",
|
||||
"crates/tuono_lib",
|
||||
"crates/tuono_lib_macros",
|
||||
]
|
||||
exclude = [
|
||||
"examples/playground"
|
||||
"apps/documentation",
|
||||
"examples/mdx",
|
||||
"examples/tuono",
|
||||
"examples/tutorial",
|
||||
"benches/tuono"
|
||||
]
|
||||
|
||||
@@ -1,56 +1,82 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png">
|
||||
</p>
|
||||
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
|
||||
<p align="center">
|
||||
⚠️ This project is under heavy development. API might drastically change ⚠️
|
||||
</p>
|
||||
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
|
||||
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
|
||||
# Tuono
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||

|
||||

|
||||
|
||||
**Tuono is the fastest React framework out there**
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Why Tuono? Just a badass name.
|
||||
|
||||
## Installation
|
||||
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
|
||||
|
||||
```
|
||||
cargo install tuono
|
||||
```
|
||||
Some of its features are:
|
||||
|
||||
## Create a new project
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
```
|
||||
tuono new
|
||||
```
|
||||
## 📖 Documentation
|
||||
|
||||
## Development
|
||||
The [documentation](https://tuono.dev/documentation) is available on
|
||||
[tuono.dev](https://tuono.dev/).
|
||||
|
||||
```
|
||||
tuono dev
|
||||
```
|
||||
## Introduction
|
||||
|
||||
## Features
|
||||
- Rust based server side rendering
|
||||
- FS routing
|
||||
- CSS modules
|
||||
- Hot Module Reload
|
||||
- Multi thread backend
|
||||
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)**
|
||||
|
||||
## Folder structure
|
||||
Tuono is a full-stack React framework with the server side written in Rust.
|
||||
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
|
||||
React is still React - it is just superpowered.
|
||||
|
||||
```
|
||||
- .tuono
|
||||
- public/
|
||||
- src/
|
||||
| - routes/
|
||||
| | - api/
|
||||
```
|
||||
**Rust is a hard language, then writing server-side code is hard as well, right? (no again)**
|
||||
|
||||
```tsx
|
||||
// Data is passed by the loading function
|
||||
const IndexPage = ({data, isLoading, isError}) => <h1>Index Page</h1>
|
||||
Tuono provides a collection of utilities to handle the server side code seamlessly with the React code.
|
||||
Each server-side route is managed in a separate file alongside the React route. The routing is handled
|
||||
by Tuono based on the files defined within the `./src/routes` directory.
|
||||
|
||||
export const Route = createFileRoute({
|
||||
loader: (params) => fetchApi(params),
|
||||
component: IndexPage
|
||||
})
|
||||
```
|
||||
**How is Tuono different from Next.js?**
|
||||
|
||||
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
|
||||
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
|
||||
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
|
||||
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
|
||||
|
||||
## Getting started
|
||||
|
||||
As already mentioned above, I strongly suggest you take a look at the
|
||||
[tutorial](https://tuono.dev/documentation/tutorial).
|
||||
|
||||
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
|
||||
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
|
||||
package manager.
|
||||
|
||||
Then run `cargo install tuono`.
|
||||
|
||||
To list all the available commands, run `tuono -h`
|
||||
|
||||
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
|
||||
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
||||
|
||||
Then, to run the local development environment, run it inside the project folder `tuono dev`
|
||||
|
||||
Finally, when the project is ready to be deployed, just run `tuono build` to create the final React assets
|
||||
and to set the server project in `production` mode.
|
||||
|
||||
Now to execute it just run `cargo run --release`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Any help or suggestion will be appreciated and encouraged.
|
||||
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Repository structure doc
|
||||
|
||||
## Monorepo tool
|
||||
- [Turborepo](https://turbo.build/repo)
|
||||
|
||||
## Packages
|
||||
> Highly inspired by [@tanstack/router](https://tanstack.com/router/latest)
|
||||
|
||||
- vite-fs-router
|
||||
- router-generator
|
||||
- react-router
|
||||
- react-router-server
|
||||
- tuono-runtime (entry point)
|
||||
|
||||
### JS Public interface
|
||||
- tuono-runtime (all in one js framework package)
|
||||
|
||||
## Crates
|
||||
- tuono (cli interface)
|
||||
- tuono-axum-builder (axum project builder)
|
||||
- tuono-watch (folder watch reload)
|
||||
|
||||
### Rust Public interface
|
||||
- tuono (cli, dev and prod environment)
|
||||
@@ -0,0 +1 @@
|
||||
# Stadalone apps folder
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"root": true,
|
||||
"reportUnusedDisableDirectives": true,
|
||||
"ignorePatterns": ["**/build", "**/dist"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"tsconfigRootDir": ".",
|
||||
"project": true,
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020,
|
||||
},
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
},
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ "prefer": "type-imports" },
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
"@typescript-eslint/method-signature-style": ["error", "property"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "typeParameter",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^(T|T[A-Z][A-Za-z]+)$",
|
||||
"match": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{ "ignoreParameters": true },
|
||||
],
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-cycle": "error",
|
||||
"import/no-duplicates": "off",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-unused-modules": "error",
|
||||
"import/order": [
|
||||
"off",
|
||||
{
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-case-declarations": "error",
|
||||
"no-empty": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.tuono
|
||||
out
|
||||
target
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.12.3"
|
||||
glob = "0.3.1"
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Tuono starter
|
||||
|
||||
This is the starter tuono project. To download it run in your terminal:
|
||||
|
||||
```shell
|
||||
$ tuono new [NAME]
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "documentation",
|
||||
"description": "The react/rust fullstack framework documentation",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/code-highlight": "^7.11.2",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "npm:tuono@0.12.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 709 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 314 KiB |
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Breadcrumbs, Button } from '@mantine/core'
|
||||
import { Link, Head } from 'tuono'
|
||||
|
||||
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
|
||||
|
||||
interface Breadcrumb {
|
||||
href?: string
|
||||
label: string
|
||||
}
|
||||
interface BreadcrumbsProps {
|
||||
breadcrumbs: Breadcrumb[]
|
||||
}
|
||||
|
||||
export default function TuonoBreadcrumbs({
|
||||
breadcrumbs = [],
|
||||
}: BreadcrumbsProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<script type="application/ld+json">
|
||||
{`{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Tuono - The React/Rust fullstack framework",
|
||||
"item": "https://tuono.dev"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Tuono - Documentation",
|
||||
"item": "https://tuono.dev/documentation"
|
||||
}${breadcrumbs.length > 0 ? ',' : ''}
|
||||
${breadcrumbs
|
||||
.map((br, i) =>
|
||||
JSON.stringify({
|
||||
'@type': 'ListItem',
|
||||
position: i + 3,
|
||||
name: br.label,
|
||||
item: br.href ? `https://tuono.dev${br.href}` : undefined,
|
||||
}),
|
||||
)
|
||||
.join(',')}]
|
||||
}
|
||||
`}
|
||||
</script>
|
||||
</Head>
|
||||
<Breadcrumbs
|
||||
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
|
||||
mb="md"
|
||||
mt="md"
|
||||
>
|
||||
<Button
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
p={5}
|
||||
>
|
||||
<IconBolt />
|
||||
</Button>
|
||||
{breadcrumbs.map((br) => (
|
||||
<BreadcrumbElement href={br.href} label={br.label} key={br.label} />
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface BreadcrumbElementProps {
|
||||
href?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export function BreadcrumbElement({
|
||||
href,
|
||||
label,
|
||||
}: BreadcrumbElementProps): JSX.Element {
|
||||
if (href) {
|
||||
return (
|
||||
<Button component={Link} href={href} variant="subtle" radius="xl" px={10}>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button component="span" variant="light" radius="xl">
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Breadcrumbs, { BreadcrumbElement } from './breadcrumbs'
|
||||
|
||||
export default Breadcrumbs
|
||||
|
||||
export { BreadcrumbElement as Element }
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Button } from '@mantine/core'
|
||||
import { IconEdit } from '@tabler/icons-react'
|
||||
import { useRouter } from 'tuono'
|
||||
|
||||
const GITHUB_URL =
|
||||
'https://github.com/Valerioageno/tuono/tree/main/apps/documentation/src/routes'
|
||||
|
||||
export default function EditPage(): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
return (
|
||||
<Button
|
||||
p={0}
|
||||
mt={60}
|
||||
component="a"
|
||||
variant="transparent"
|
||||
leftSection={<IconEdit />}
|
||||
target="_blank"
|
||||
href={GITHUB_URL.concat(pathname).concat('.mdx')}
|
||||
>
|
||||
Edit page
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import EditPage from './edit-page'
|
||||
|
||||
export default EditPage
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
CopyButton,
|
||||
Group,
|
||||
rem,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { IconCopy, IconCheck } from '@tabler/icons-react'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function Hero(): JSX.Element {
|
||||
return (
|
||||
<Container size={1100} my={100}>
|
||||
<Center>
|
||||
<Title order={1}>Superfast fullstack react framework</Title>
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Text fz="18px">
|
||||
The technologies we love seamlessly working together to unleash the
|
||||
<strong> highest web performance</strong> ever met on React
|
||||
</Text>
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Group>
|
||||
<Button component={Link} href="/documentation" size="lg">
|
||||
Get Started
|
||||
</Button>
|
||||
<CopyButton value="cargo install tuono">
|
||||
{({ copied, copy }) => (
|
||||
<Button
|
||||
onClick={copy}
|
||||
size="lg"
|
||||
style={{ border: 'solid 1px var(--mantine-color-violet-1)' }}
|
||||
color="gray"
|
||||
leftSection="$ cargo install tuono"
|
||||
rightSection={
|
||||
copied ? (
|
||||
<IconCheck style={{ width: rem(20) }} />
|
||||
) : (
|
||||
<IconCopy style={{ width: rem(20) }} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Hero from './hero'
|
||||
|
||||
export default Hero
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxProvider from './mdx-provider'
|
||||
|
||||
export default MdxProvider
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Text, type TextProps } from '@mantine/core'
|
||||
|
||||
export default function MdxBold(props: TextProps): JSX.Element {
|
||||
return <Text fw={700} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxCode from './mdx-code'
|
||||
|
||||
export default MdxCode
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Code } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxCode(
|
||||
props: HTMLAttributes<HTMLPreElement>,
|
||||
): JSX.Element {
|
||||
return <Code {...props} style={{ fontSize: 14 }} />
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxLink from './mdx-link'
|
||||
|
||||
export default MdxLink
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { JSX, AnchorHTMLAttributes } from 'react'
|
||||
import { Button } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { IconExternalLink } from '@tabler/icons-react'
|
||||
|
||||
export default function MdxLink(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
{...props}
|
||||
target="_blank"
|
||||
rightSection={
|
||||
<IconExternalLink size="16px" style={{ marginLeft: -5 }} />
|
||||
}
|
||||
variant="transparent"
|
||||
style={{ height: '20px' }}
|
||||
mt={-2}
|
||||
p={0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
component={Link}
|
||||
{...props}
|
||||
target="_blank"
|
||||
variant="transparent"
|
||||
style={{ height: '20px' }}
|
||||
mt={-2}
|
||||
p={0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxPre from './mdx-pre'
|
||||
|
||||
export default MdxPre
|
||||
@@ -0,0 +1,5 @@
|
||||
.pre {
|
||||
code {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { JSX } from 'react'
|
||||
import { CodeHighlight } from '@mantine/code-highlight'
|
||||
import styles from './mdx-pre.module.css'
|
||||
|
||||
interface PreProps {
|
||||
children: any
|
||||
}
|
||||
export default function MdxPre({ children }: PreProps): JSX.Element {
|
||||
return (
|
||||
<CodeHighlight
|
||||
className={styles.pre}
|
||||
code={children.props.children || ''}
|
||||
style={{ borderRadius: 8, fontWeight: 100 }}
|
||||
language={children.props.className?.replace('language-', '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { JSX } from 'react'
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
|
||||
import MdxLink from './mdx-link'
|
||||
import type { ReactNode } from 'react'
|
||||
import MdxPre from './mdx-pre'
|
||||
import MdxQuote from './mdx-quote'
|
||||
import MdxCode from './mdx-code'
|
||||
import { h } from './mdx-title'
|
||||
import MdxBold from './mdx-bold/mdx-bold'
|
||||
|
||||
interface MdxProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function MdxProvider({
|
||||
children,
|
||||
}: MdxProviderProps): JSX.Element {
|
||||
return (
|
||||
<MDXProvider
|
||||
components={{
|
||||
a: MdxLink,
|
||||
// @ts-expect-error: useless finding the correct props types
|
||||
pre: MdxPre,
|
||||
blockquote: MdxQuote,
|
||||
code: MdxCode,
|
||||
h1: h(1),
|
||||
h2: h(2),
|
||||
h3: h(3),
|
||||
h4: h(4),
|
||||
h5: h(5),
|
||||
h6: h(6),
|
||||
strong: MdxBold,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MDXProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxQuote from './mdx-quote'
|
||||
|
||||
export default MdxQuote
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { JSX, HTMLAttributes } from 'react'
|
||||
import { Blockquote, Space } from '@mantine/core'
|
||||
|
||||
export default function MdxQuote(
|
||||
props: HTMLAttributes<HTMLQuoteElement>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Blockquote
|
||||
color="violet"
|
||||
py={1}
|
||||
px={20}
|
||||
mt={30}
|
||||
iconSize={30}
|
||||
{...props}
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
<Space h="md" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import MdxTitle, { h } from './mdx-title'
|
||||
|
||||
export default MdxTitle
|
||||
|
||||
export { h }
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Title, type TitleProps } from '@mantine/core'
|
||||
|
||||
export default function MdxTitle(props: TitleProps): JSX.Element {
|
||||
return (
|
||||
<Title
|
||||
data-heading={props.children}
|
||||
data-order={props.order}
|
||||
mt={20}
|
||||
{...props}
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const h =
|
||||
(order: 1 | 2 | 3 | 4 | 5 | 6) =>
|
||||
(props: TitleProps): JSX.Element => <MdxTitle order={order} {...props} />
|
||||
@@ -0,0 +1,3 @@
|
||||
import MetaTags from './meta-tags'
|
||||
|
||||
export default MetaTags
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Head } from 'tuono'
|
||||
|
||||
interface MetaTagsProps {
|
||||
title: string
|
||||
description?: string
|
||||
type?: 'website' | 'article' | 'book' | 'video.movie' | 'music.song'
|
||||
canonical: string
|
||||
}
|
||||
|
||||
export default function MetaTags({
|
||||
title,
|
||||
description,
|
||||
type = 'article',
|
||||
canonical,
|
||||
}: MetaTagsProps): ReactNode {
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<link rel="canonical" href={canonical} />
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
description ??
|
||||
'The technologies we love seamlessly working together to unleash the highest web performance ever met on React'
|
||||
}
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="ReactJs, Typescript, Rust, JavaScript, Fullstack framework, Web development"
|
||||
/>
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:image" content="https://tuono.dev/og-cover.png" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:site_name" content="Tuono" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@tuono_dev" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="https://tuono.dev/og-cover.png" />
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||
import {
|
||||
IconBrandGithub,
|
||||
IconBook,
|
||||
IconBrandDiscord,
|
||||
} from '@tabler/icons-react'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
import ThemeBtn from '../theme-btn'
|
||||
|
||||
export default function Actions(): JSX.Element {
|
||||
return (
|
||||
<Flex gap={8}>
|
||||
<Group gap={8}>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/documentation"
|
||||
visibleFrom="sm"
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
autoContrast
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Check the project on github"
|
||||
href="https://github.com/Valerioageno/tuono"
|
||||
target="_blank"
|
||||
component="a"
|
||||
>
|
||||
<IconBrandGithub />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Join the Tuono's community on Discord!"
|
||||
href="https://discord.com/invite/khQzPa654B"
|
||||
target="_blank"
|
||||
component="a"
|
||||
>
|
||||
<IconBrandDiscord />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ThemeBtn />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Navbar from './navbar'
|
||||
|
||||
export default Navbar
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
|
||||
import Actions from './actions'
|
||||
|
||||
interface NavbarProps {
|
||||
opened: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
return (
|
||||
<AppShell.Header p="sm">
|
||||
<Flex justify="space-between">
|
||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||
Tuono
|
||||
</Button>
|
||||
<Flex align="center" gap={8}>
|
||||
<Actions />
|
||||
{pathname.startsWith('/documentation') && (
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import NavigationButtons from './navigation-buttons'
|
||||
|
||||
export default NavigationButtons
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Box, Button, Text, Title, Flex } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { IconArrowRight, IconArrowLeft } from '@tabler/icons-react'
|
||||
|
||||
interface NavigationButton {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface NavigationButtonsProps {
|
||||
prev?: NavigationButton
|
||||
next?: NavigationButton
|
||||
}
|
||||
|
||||
export default function NavigationButtons({
|
||||
prev,
|
||||
next,
|
||||
}: NavigationButtonsProps): JSX.Element {
|
||||
return (
|
||||
<Flex mt={50} gap={10}>
|
||||
{prev && <NavigationBtn type="prev" {...prev} />}
|
||||
{next && <NavigationBtn type="next" {...next} />}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavigationButtonProps extends NavigationButton {
|
||||
type: 'next' | 'prev'
|
||||
}
|
||||
|
||||
const NavigationBtn = ({
|
||||
type,
|
||||
title,
|
||||
href,
|
||||
}: NavigationButtonProps): JSX.Element => {
|
||||
const heading = type === 'next' ? 'Next' : 'Previous'
|
||||
const textAlign = type === 'next' ? 'left' : 'right'
|
||||
const variant = type === 'next' ? 'filled' : 'outline'
|
||||
|
||||
return (
|
||||
<Button
|
||||
component={Link}
|
||||
fullWidth
|
||||
variant={variant}
|
||||
href={href}
|
||||
justify="space-between"
|
||||
h="auto"
|
||||
rightSection={type === 'next' && <IconArrowRight />}
|
||||
leftSection={type === 'prev' && <IconArrowLeft />}
|
||||
p="20"
|
||||
>
|
||||
<Box>
|
||||
<Title component="span" display="block" order={4} style={{ textAlign }}>
|
||||
{heading}
|
||||
</Title>
|
||||
<Text component="span" display="block" style={{ textAlign }}>
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Sidebar from './sidebar'
|
||||
|
||||
export default Sidebar
|
||||
@@ -0,0 +1,6 @@
|
||||
.link {
|
||||
border-radius: 8px;
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { JSX, ReactNode } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { NavLink, type NavLinkProps } from '@mantine/core'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { IconChevronRight } from '@tabler/icons-react'
|
||||
|
||||
import styles from './sidebar-link.module.css'
|
||||
|
||||
interface SidebarLinkProps {
|
||||
label: string
|
||||
href: string
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function SidebarLink(
|
||||
props: SidebarLinkProps & NavLinkProps,
|
||||
): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
|
||||
|
||||
const internalProps = {
|
||||
active: pathname === props.href,
|
||||
className: styles.link,
|
||||
rightSection: props.children && (
|
||||
<IconChevronRight
|
||||
size="1.2rem"
|
||||
stroke={1.5}
|
||||
className="mantine-rotate-rtl"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
opened: isOpen,
|
||||
autoContrast: true,
|
||||
...props,
|
||||
}
|
||||
|
||||
if (props.href.startsWith('#')) {
|
||||
return <NavLink component="button" {...internalProps} />
|
||||
}
|
||||
|
||||
return <NavLink component={Link} {...internalProps} />
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell } from '@mantine/core'
|
||||
|
||||
import SidebarLink from './sidebar-link'
|
||||
|
||||
interface SidebarProps {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export default function Sidebar({ close }: SidebarProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<SidebarLink
|
||||
href="/documentation"
|
||||
label="Getting started"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/installation"
|
||||
label="Installation"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial"
|
||||
label="Tutorial"
|
||||
defaultOpened
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/development-setup"
|
||||
label="Development setup"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/api-fetching"
|
||||
label="API fetching"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/components"
|
||||
label="Components"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/dynamic-routes"
|
||||
label="Dynamic routes"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/error-handling"
|
||||
label="Error handling"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/seo"
|
||||
label="SEO and meta tags"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/redirections"
|
||||
label="Server redirection"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/production"
|
||||
label="Production build"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/conclusion"
|
||||
label="Conclusion"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
<SidebarLink href="/documentation/cli" label="CLI" onClick={close} />
|
||||
<SidebarLink
|
||||
label="Application state"
|
||||
href="/documentation/application-state"
|
||||
onClick={close}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Routing"
|
||||
href="/documentation/routing"
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/routing/intro"
|
||||
label="Project structure"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
label="Contributing"
|
||||
href="/documentation/contributing"
|
||||
onClick={close}
|
||||
leftSection="✨"
|
||||
/>
|
||||
</AppShell.Navbar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
|
||||
*/
|
||||
export interface Heading {
|
||||
depth: number
|
||||
content: string
|
||||
id: string
|
||||
getNode: () => HTMLHeadingElement
|
||||
}
|
||||
|
||||
function getHeadingsData(headings: HTMLHeadingElement[]): Heading[] {
|
||||
const result: Heading[] = []
|
||||
|
||||
for (const heading of headings) {
|
||||
if (heading.id) {
|
||||
result.push({
|
||||
depth: parseInt(heading.getAttribute('data-order') || '1', 10),
|
||||
content: heading.getAttribute('data-heading') || '',
|
||||
id: heading.id,
|
||||
getNode: () =>
|
||||
document.getElementById(heading.id) as HTMLHeadingElement,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getHeadings(): Heading[] {
|
||||
const root = document.getElementById('mdx-root')
|
||||
console.log(root)
|
||||
|
||||
if (!root) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getHeadingsData(Array.from(root.querySelectorAll('[data-heading]')))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
|
||||
*/
|
||||
import { TableOfContents } from './table-of-content'
|
||||
|
||||
export default TableOfContents
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
|
||||
*/
|
||||
.wrapper {
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
position: sticky;
|
||||
top: var(--mantine-spacing-xl);
|
||||
right: 0;
|
||||
padding-top: 55px;
|
||||
flex: 0 0 calc(var(--docs-table-of-contents-width) - 20px);
|
||||
width: 200px;
|
||||
|
||||
@mixin rtl {
|
||||
padding-left: 0;
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&[data-with-tabs] {
|
||||
padding-top: 0;
|
||||
top: calc(var(--mantine-spacing-xl) + 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding-bottom: var(--mantine-spacing-xl);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
@mixin rtl {
|
||||
padding-left: 0;
|
||||
padding-right: var(--mantine-spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: block;
|
||||
border-left: 1px solid transparent;
|
||||
padding: 8px var(--mantine-spacing-md);
|
||||
margin-left: -1px;
|
||||
padding-left: calc(var(--toc-link-offset) * var(--mantine-spacing-lg));
|
||||
border-top-right-radius: var(--mantine-radius-sm);
|
||||
border-bottom-right-radius: var(--mantine-radius-sm);
|
||||
|
||||
@mixin light {
|
||||
color: var(--mantine-color-gray-7);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
color: var(--mantine-color-dark-1);
|
||||
}
|
||||
|
||||
@mixin rtl {
|
||||
border-left: 0;
|
||||
border-right: 1px solid transparent;
|
||||
margin-left: 0;
|
||||
margin-right: -1px;
|
||||
border-top-left-radius: var(--mantine-radius-sm);
|
||||
border-bottom-left-radius: var(--mantine-radius-sm);
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
border-color: var(--mantine-color-violet-5);
|
||||
|
||||
@mixin light {
|
||||
color: var(--mantine-color-violet-8);
|
||||
background-color: var(--mantine-color-violet-0);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
color: var(--mantine-color-violet-1);
|
||||
background-color: #534a6d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: var(--mantine-spacing-md);
|
||||
|
||||
@mixin rtl {
|
||||
margin-left: 0;
|
||||
margin-right: var(--mantine-spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.items {
|
||||
border-left: 1px solid;
|
||||
|
||||
@mixin light {
|
||||
border-color: var(--mantine-color-gray-3) !important;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
border-color: var(--mantine-color-dark-4) !important;
|
||||
}
|
||||
|
||||
@mixin rtl {
|
||||
border-left: 0;
|
||||
border-right: 1px solid;
|
||||
|
||||
@mixin light {
|
||||
border-color: var(--mantine-color-gray-3) !important;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
border-color: var(--mantine-color-dark-4) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
|
||||
*/
|
||||
import type { JSX } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter, Link } from 'tuono'
|
||||
import { IconList } from '@tabler/icons-react'
|
||||
import { Box, rem, ScrollArea, Text } from '@mantine/core'
|
||||
import { getHeadings, type Heading } from './get-headings'
|
||||
import classes from './table-of-content.module.css'
|
||||
|
||||
interface TableOfContentsProps {
|
||||
withTabs: boolean
|
||||
}
|
||||
|
||||
function getActiveElement(rects: DOMRect[]): number {
|
||||
if (rects.length === 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
const closest = rects.reduce(
|
||||
(acc, item, index) => {
|
||||
if (Math.abs(acc.position) < Math.abs(item.y)) {
|
||||
return acc
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
position: item.y,
|
||||
}
|
||||
},
|
||||
{ index: 0, position: rects[0].y },
|
||||
)
|
||||
|
||||
return closest.index
|
||||
}
|
||||
|
||||
export function TableOfContents({
|
||||
withTabs,
|
||||
}: TableOfContentsProps): JSX.Element | null {
|
||||
const [active, setActive] = useState(0)
|
||||
const [headings, setHeadings] = useState<Heading[]>([])
|
||||
const headingsRef = useRef<Heading[]>([])
|
||||
const router = useRouter()
|
||||
|
||||
const filteredHeadings = headings.filter((heading) => heading.depth > 1)
|
||||
|
||||
const handleScroll = (): void => {
|
||||
setActive(
|
||||
getActiveElement(
|
||||
headingsRef.current.map((d) => d.getNode().getBoundingClientRect()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const _headings = getHeadings()
|
||||
headingsRef.current = _headings
|
||||
setHeadings(_headings)
|
||||
setActive(
|
||||
getActiveElement(
|
||||
_headings.map((d) => d.getNode().getBoundingClientRect()),
|
||||
),
|
||||
)
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return (): void => window.removeEventListener('scroll', handleScroll)
|
||||
}, [router.pathname])
|
||||
|
||||
if (filteredHeadings.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const items = filteredHeadings.map((heading, index) => (
|
||||
<Text
|
||||
key={heading.id}
|
||||
component={Link}
|
||||
fz="sm"
|
||||
p={10}
|
||||
className={classes.link}
|
||||
mod={{ active: active === index }}
|
||||
href={`#${heading.id}`}
|
||||
__vars={{ '--toc-link-offset': `${heading.depth - 1}` }}
|
||||
>
|
||||
{heading.content}
|
||||
</Text>
|
||||
))
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="nav"
|
||||
mod={{ 'with-tabs': withTabs }}
|
||||
className={classes.wrapper}
|
||||
>
|
||||
<div className={classes.inner}>
|
||||
<div>
|
||||
<div className={classes.header}>
|
||||
<IconList
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
<Text className={classes.title}>Table of contents</Text>
|
||||
</div>
|
||||
<ScrollArea.Autosize
|
||||
mah={`calc(100vh - ${rem(140)})`}
|
||||
type="never"
|
||||
offsetScrollbars
|
||||
>
|
||||
<div className={classes.items}>{items}</div>
|
||||
</ScrollArea.Autosize>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import ThemeBtn from './theme-btn'
|
||||
|
||||
export default ThemeBtn
|
||||
@@ -0,0 +1,24 @@
|
||||
.icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@mixin dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
@mixin light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
ActionIcon,
|
||||
useMantineColorScheme,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core'
|
||||
import { IconSun, IconMoon } from '@tabler/icons-react'
|
||||
import cx from 'clsx'
|
||||
|
||||
import classes from './theme-btn.module.css'
|
||||
|
||||
export default function ThemeBtn(): JSX.Element {
|
||||
const { setColorScheme } = useMantineColorScheme()
|
||||
const computedColorScheme = useComputedColorScheme('light', {
|
||||
getInitialValueInEffect: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
|
||||
}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Toggle color scheme"
|
||||
>
|
||||
<IconSun className={cx(classes.icon, classes.light)} stroke={1.5} />
|
||||
<IconMoon className={cx(classes.icon, classes.dark)} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// declaration.d.ts
|
||||
declare module '*.css'
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
createTheme,
|
||||
MantineProvider,
|
||||
AppShell,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { Head, useRouter } from 'tuono'
|
||||
import Navbar from '../components/navbar'
|
||||
|
||||
import '@mantine/core/styles.css'
|
||||
import '@mantine/code-highlight/styles.css'
|
||||
|
||||
import Sidebar from '../components/sidebar'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const theme = createTheme({
|
||||
primaryColor: 'violet',
|
||||
primaryShade: { light: 6, dark: 9 },
|
||||
fontFamily: 'Roboto',
|
||||
respectReducedMotion: true,
|
||||
radius: {
|
||||
xs: '8px',
|
||||
lg: '8px',
|
||||
xl: '8px',
|
||||
md: '8px',
|
||||
sm: '8px',
|
||||
},
|
||||
fontSizes: {
|
||||
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
xs: '16px',
|
||||
sm: '16px',
|
||||
},
|
||||
colors: {
|
||||
dark: [
|
||||
'#d5d7e0',
|
||||
'#acaebf',
|
||||
'#8c8fa3',
|
||||
'#666980',
|
||||
'#4d4f66',
|
||||
'#34354a',
|
||||
'#2b2c3d',
|
||||
'#1d1e30',
|
||||
'#0c0d21',
|
||||
'#01010a',
|
||||
],
|
||||
},
|
||||
headings: {
|
||||
sizes: {
|
||||
h1: {
|
||||
fontSize: '48px',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<ColorSchemeScript />
|
||||
<MantineProvider theme={theme}>
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
import { AppShell, Container } from '@mantine/core'
|
||||
|
||||
import MdxProvider from '../../components/mdx-provider'
|
||||
import EditPage from '../../components/edit-page'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Main>
|
||||
<Container id="mdx-root" component="article" size="md" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Application state"
|
||||
canonical="https://tuono.dev/documentation/application-state"
|
||||
description="Learn how to add features to your Tuono application"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
|
||||
|
||||
# Application state
|
||||
|
||||
The main reason Tuono is fast is that it loads just the features that are needed for the project.
|
||||
|
||||
To define them, you need to fill the `ApplicationState` struct in the `./src/main.rs` file, and
|
||||
they will be automatically available in all the handlers you will define across the application.
|
||||
|
||||
```rs
|
||||
// src/main.rs
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApplicationState {
|
||||
pub website_name: String,
|
||||
pub base_path: String
|
||||
}
|
||||
|
||||
pub fn main() -> ApplicationState {
|
||||
let website_name = "tuono".to_string();
|
||||
let base_path = "http://localhost:3000".to_string();
|
||||
|
||||
ApplicationState {
|
||||
website_name,
|
||||
base_path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> For the sake of simplicity here we just enable a `String` but you can add Database connections or
|
||||
> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client).
|
||||
|
||||
Now the `ApplicationState` is available on all the handlers as following:
|
||||
|
||||
```rs
|
||||
// src/routes/index.rs
|
||||
|
||||
#[tuono_lib::handler]
|
||||
// The first argument always is the Request
|
||||
// The ApplicationState arguments are optional. You can use just the ones you need
|
||||
// to use in the handler (with no specific order).
|
||||
async fn my_handler(req: Request, website_name: String) -> Response {
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - CLI"
|
||||
canonical="https://tuono.dev/documentation/cli"
|
||||
description="Tuono is the CLI that provides all the needed commands to handle the full-stack project."
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
|
||||
|
||||
# CLI
|
||||
|
||||
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
|
||||
|
||||
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
|
||||
> `tuono` CLI yet.
|
||||
|
||||
To list all the available commands, run `tuono -h`.
|
||||
|
||||
```bash
|
||||
The React/Rust full-stack framework
|
||||
|
||||
Usage: tuono <COMMAND>
|
||||
|
||||
Commands:
|
||||
dev Start the development environment
|
||||
build Build the production assets
|
||||
new Scaffold a new project
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
To create a new project, run `tuono new [NAME]` (optionally you can pass the --template (or -t) flag - check the examples' folder).
|
||||
|
||||
Then to run the local development environment run inside the project folder `tuono dev`
|
||||
|
||||
Finally, when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
|
||||
|
||||
Now to execute it, just run `cargo run --release`.
|
||||
@@ -0,0 +1,48 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Contributing"
|
||||
canonical="https://tuono.dev/documentation/contributing"
|
||||
description="The project is massive - if you like it, do consider contributing!"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
|
||||
|
||||
# Contributing
|
||||
|
||||
## TL;DR
|
||||
|
||||
The project is massive - if you like it, do consider contributing!
|
||||
|
||||
## Getting started
|
||||
|
||||
The `tuono` project can mostly be split into the following subdomains:
|
||||
|
||||
- The CLI
|
||||
- The Rust backend
|
||||
- The React frontend
|
||||
- The documentation website (which is written with tuono 🚀)
|
||||
|
||||
To check what the knowledge requirements are for each domain, check the
|
||||
[requirements](#requirements) section below.
|
||||
|
||||
Currently, I'm keeping a private dashboard to prioritize new features and bug fixes, but if you
|
||||
want to propose something, please open a new issue on Github or reach out to me using
|
||||
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
|
||||
on Twitter (X) DMs `@valerioageno`, [Linkedin](https://www.linkedin.com/in/valerioageno)
|
||||
and discord `@__v__v__`.
|
||||
|
||||
## Requirements
|
||||
|
||||
It's not strictly required to know both React (& typescript) and Rust (even though it
|
||||
would be a great nice to have).
|
||||
|
||||
Without taking into account specific cases, we can mostly split the domain requirements by:
|
||||
|
||||
- The `CLI` needs Rust knowledge (even though a couple of scenarios might also need Typescript)
|
||||
- The Backend needs just `Rust`
|
||||
- The Frontend needs just `React` & `Typescript`
|
||||
- The documentation website needs just `React` & `Typescript` (or even less, since most of the
|
||||
code is markdown).
|
||||
@@ -0,0 +1,54 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Documentation"
|
||||
canonical="https://tuono.dev/documentation"
|
||||
description="Tuono is a full-stack framework for building React applications using Rust as the backend."
|
||||
/>
|
||||
|
||||
import Breadcrumbs from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs />
|
||||
|
||||
# Tuono
|
||||
|
||||
Tuono is a full-stack web framework for building React applications using Rust as the
|
||||
backend with a strong focus on usability and performance.
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
|
||||
|
||||
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
|
||||
|
||||
## Features
|
||||
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
## Introduction
|
||||
|
||||
Tuono addresses the following two limits:
|
||||
|
||||
- Enable React server-side rendering projects without using JS runtimes (Node, Deno)
|
||||
- Ease the web development using a typically hard language like Rust.
|
||||
|
||||
#### NodeJs/Deno/Bun are the only runtimes that allow a React app to be server-side rendered (SSR)? (no)
|
||||
|
||||
Tuono is the first full-stack React framework with the server side written in Rust (check the [ssr-rs](https://github.com/Valerioageno/ssr-rs) crate).
|
||||
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
|
||||
React is still React - it is just superpowered.
|
||||
|
||||
#### Rust is a hard language, then writing server side code is hard as well, right? (no again)
|
||||
|
||||
Tuono provides a collection of utilities to handle server-side code seamlessly with
|
||||
the React code. Each server side route is managed in a separate file alongside the React route.
|
||||
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
|
||||
|
||||
#### How is Tuono different from Next.js?
|
||||
|
||||
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
|
||||
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
|
||||
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
|
||||
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
|
||||
@@ -0,0 +1,57 @@
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Installation"
|
||||
canonical="https://tuono.dev/documentation/installation"
|
||||
description="Tuono is a development environment built in rust and typescript that outputs a website written in both the languages"
|
||||
/>
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Installation' }]} />
|
||||
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
`Tuono` is a development environment built in rust and typescript that outputs a website written in both the
|
||||
languages. Since then, you need the following tools installed on your computer to work with it:
|
||||
|
||||
- `Rust` - The Rust programming language tool chain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
|
||||
- `Cargo` - The rust package manager
|
||||
- `NodeJs` - The JavaScript runtime (Go [here](https://nodejs.org/en/download/package-manager) for installing both `nodejs` and `npm`)
|
||||
- `npm` or `yarn` or `pnpm` - A javaScript package manager
|
||||
|
||||
> Node.js is needed just for the development environment. The final output will run on a Rust server.
|
||||
|
||||
## Installation
|
||||
|
||||
The tuono `CLI` is hosted on [crates.io](https://crates.io/crates/tuono) to download and install it, just run it on a terminal:
|
||||
|
||||
```bash
|
||||
cargo install tuono
|
||||
```
|
||||
|
||||
To check that it is correctly installed, run:
|
||||
|
||||
```bash
|
||||
tuono --version
|
||||
```
|
||||
|
||||
Run `tuono -h` to see all the available commands.
|
||||
|
||||
```bash
|
||||
The React/Rust full-stack framework
|
||||
|
||||
Usage: tuono <COMMAND>
|
||||
|
||||
Commands:
|
||||
dev Start the development environment
|
||||
build Build the production assets
|
||||
new Scaffold a new project
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/intro"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
|
||||
|
||||
# Routing index
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,19 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Routing', href: '/documentation/routing' },
|
||||
{ label: 'Project structure' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Project structure
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,189 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - API fetching"
|
||||
canonical="https://tuono.dev/documentation/tutorial/api-fetching"
|
||||
description="Learn how to fetch remote APIs on the backend"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'API fetching' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# API fetching
|
||||
|
||||
The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the Pokémon of the first generation (the best one, btw) and then reserve a dynamic page for each one separately.
|
||||
|
||||
## Fetch all the Pokémon
|
||||
|
||||
To start, let’s fetch all of them from the root page. Since we want to render them on the server side, we are going to need to implement the logic in the `index.rs` file.
|
||||
|
||||
Clear the `index.rs` file and paste:
|
||||
|
||||
```rust
|
||||
// src/routes/index.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use reqwest::Client;
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemons {
|
||||
results: Vec<Pokemon>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||
return match fetch.get(ALL_POKEMON).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemons>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Props::new("{}")),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers.
|
||||
> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional.
|
||||
|
||||
The terminal will complain now for two reasons:
|
||||
|
||||
1. We don't have imported any `reqwest` crate
|
||||
2. The second argument `fetch: Client` has not been defined yet as global state.
|
||||
|
||||
Let's fix these in the next section.
|
||||
|
||||
## Application state
|
||||
|
||||
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
|
||||
|
||||
You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application.
|
||||
|
||||
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
|
||||
|
||||
To install it just run in your terminal:
|
||||
|
||||
```bash
|
||||
$ cargo add reqwest
|
||||
```
|
||||
|
||||
A new entry has just been added to your `Cargo.toml` file.
|
||||
|
||||
```diff
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.14.0" # the version might be different
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
++ reqwest = "0.12.9" # the version might be different
|
||||
```
|
||||
|
||||
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
|
||||
> (similarly as the package.json for Javascript).
|
||||
|
||||
Now let's define the `ApplicationState` in the `./src/main.rs` file.
|
||||
|
||||
```rs
|
||||
// Import here the just added reqwest library
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Clone)]
|
||||
// Extend this struct with the feature you will need for your application
|
||||
pub struct ApplicationState {
|
||||
// This will be available to all your route handlers
|
||||
pub fetch: Client,
|
||||
}
|
||||
|
||||
pub fn main() -> ApplicationState {
|
||||
let fetch = Client::new();
|
||||
return ApplicationState { fetch };
|
||||
}
|
||||
```
|
||||
|
||||
Now the `fetch: Client` argument is available in the above defined handler, and the terminal should not complain anymore.
|
||||
Let's see in the next section how to show the fetched data on the browser.
|
||||
|
||||
## Handling the page UI
|
||||
|
||||
Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste:
|
||||
|
||||
```tsx
|
||||
// src/routes/index.tsx
|
||||
import type { JSX } from 'react'
|
||||
import type { TuonoProps } from 'tuono'
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface IndexProps {
|
||||
results: Pokemon[]
|
||||
}
|
||||
|
||||
export default function IndexPage({
|
||||
data,
|
||||
}: TuonoProps<IndexProps>): JSX.Element | null {
|
||||
if (!data?.results) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||
Crates
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tuono" target="_blank">
|
||||
Npm
|
||||
</a>
|
||||
</header>
|
||||
<div className="title-wrap">
|
||||
<h1 className="title">
|
||||
TU<span>O</span>NO
|
||||
</h1>
|
||||
<div className="logo">
|
||||
<img src="rust.svg" className="rust" />
|
||||
<img src="react.svg" className="react" />
|
||||
</div>
|
||||
</div>
|
||||
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
|
||||
{data.results.map((pokemon) => {
|
||||
return pokemon.name
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Refresh the browser now! A bit ugly, but all the Pokémon are finally printed on screen!
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{
|
||||
title: 'Development setup',
|
||||
href: '/documentation/tutorial/development-setup',
|
||||
}}
|
||||
next={{ title: 'Components', href: '/documentation/tutorial/components' }}
|
||||
/>
|
||||
@@ -0,0 +1,145 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Components"
|
||||
canonical="https://tuono.dev/documentation/tutorial/components"
|
||||
description="Learn how to manage the components in a Tuono codebase"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Components' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Components
|
||||
|
||||
## Creating a stand-alone component
|
||||
|
||||
Let’s then create the button needed for displaying the list of Pokémon.
|
||||
|
||||
Create the following file `src/components/PokemonLink.tsx` and fill the content with:
|
||||
|
||||
```tsx
|
||||
// src/components/PokemonLink.tsx
|
||||
import type { JSX } from 'react'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function PokemonLink({
|
||||
pokemon,
|
||||
id,
|
||||
}: {
|
||||
pokemon: Pokemon
|
||||
id: number
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Link href={`/pokemons/${pokemon.name}`}>
|
||||
{pokemon.name}
|
||||
<img
|
||||
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now that the link is done, let’s import it into the `index.tsx` file
|
||||
|
||||
```diff
|
||||
// src/routes/index.tsx
|
||||
|
||||
++ import PokemonLink from '../components/PokemonLink'
|
||||
|
||||
// ...
|
||||
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
|
||||
-- {pokemons.map((pokemon) => {
|
||||
-- return pokemon.name;
|
||||
++ {pokemons.map((pokemon, i) => {
|
||||
++ return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />;
|
||||
})}
|
||||
</ul>
|
||||
// ...
|
||||
```
|
||||
|
||||
Now the links work. Clicking on any of them, we get redirected to the 404 page because we haven’t yet implemented the `pokemons/[pokemon]` page.
|
||||
As previously said, CSS modules are enabled out of the box, so let’s make those links a little bit nicer.
|
||||
|
||||
Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it:
|
||||
|
||||
```css
|
||||
/* src/components/PokemonLink.module.css */
|
||||
|
||||
.link {
|
||||
width: 100%;
|
||||
max-width: 216px;
|
||||
position: relative;
|
||||
background: white;
|
||||
margin-bottom: 10px;
|
||||
border: solid #f0f0f0 1px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
padding: 5px 5px 5px 15px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: 0.2s;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
||||
}
|
||||
|
||||
.link img {
|
||||
width: 70px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 SASS is supported out of the box. Just install the processor in the devDependencies `npm i -D sass` and run again `tuono dev`
|
||||
|
||||
Then import the styles into the `PokemonLink` component as following:
|
||||
|
||||
```diff
|
||||
// src/components/PokemonLink.tsx
|
||||
import type { JSX } from 'react'
|
||||
import { Link } from "tuono"
|
||||
import type { Pokemon } from "./../types/pokemon"
|
||||
++ import styles from './PokemonLink.module.css'
|
||||
|
||||
export default function PokemonLink({
|
||||
pokemon,
|
||||
id,
|
||||
}: {
|
||||
pokemon: Pokemon;
|
||||
id: number;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
-- <Link href={`/pokemons/${pokemon.name}`}>
|
||||
++ <Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
|
||||
{pokemon.name}
|
||||
<img
|
||||
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{ title: 'API fetching', href: '/documentation/tutorial/api-fetching' }}
|
||||
next={{
|
||||
title: 'Dynamic routes',
|
||||
href: '/documentation/tutorial/dynamic-routes',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Conclusion"
|
||||
canonical="https://tuono.dev/documentation/tutorial/conclusion"
|
||||
description="You just created a multi thread full stack application with Rust and React"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Conclusion' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Conclusion
|
||||
|
||||
That’s it! You just created a multi thread full stack application with Rust and React.
|
||||
|
||||
The project is still under heavy development and many features are not ready yet, but
|
||||
I hope you got the taste of what is like working with rust and react in the same stack.
|
||||
|
||||
As I mentioned in the introduction, I'd love to hear what you thought about the framework and the tutorial - feel free to reach me
|
||||
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or in Twitter (X) DMs [@valerioageno](https://twitter.com/valerioageno).
|
||||
|
||||
Ciao
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{
|
||||
title: 'Production build',
|
||||
href: '/documentation/tutorial/production',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,77 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Development setup"
|
||||
canonical="https://tuono.dev/documentation/tutorial/development-setup"
|
||||
description="Learn how to setup a new Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Development setup' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Development setup
|
||||
|
||||
## Scaffold a new project
|
||||
|
||||
To set up a new fresh project you just need to run the following command:
|
||||
|
||||
```
|
||||
$ tuono new tuono-tutorial
|
||||
```
|
||||
|
||||
Get into the project folder and install the dependencies with:
|
||||
|
||||
```
|
||||
$ npm install
|
||||
```
|
||||
|
||||
Open it with your favorite code editor.
|
||||
|
||||
The project will have the following structure:
|
||||
|
||||
```
|
||||
├── package.json
|
||||
├── public
|
||||
├── src
|
||||
│ ├── routes
|
||||
│ └── styles
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
`public/`: put here all the files you want to be public
|
||||
|
||||
`src/routes/`: All the files in this folder are considered routes. All the routes are server side rendered out of the box.
|
||||
To add server side capabilities, just create a Rust file with the same name as the route (i.e. `about.tsx` → `about.rs`).
|
||||
|
||||
`src/styles/`: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project, you can use
|
||||
CSS modules.
|
||||
|
||||
## Start the dev environment
|
||||
|
||||
To start the development environment, you just need to run the following command within the project folder:
|
||||
|
||||
```
|
||||
$ tuono dev
|
||||
```
|
||||
|
||||
The first time might take a little bit because it will install all the Rust’s dependencies. All the other execution will be pretty quick!
|
||||
|
||||
> 💡 The tuono dev development script is currently under strong optimization improvements.
|
||||
> In case you face any error delete the cache `.tuono` folder and run it again!
|
||||
|
||||
Then open [http://localhost:3000/](http://localhost:3000) on the browser.
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{ title: 'Tutorial', href: '/documentation/tutorial' }}
|
||||
next={{ title: 'API fetching', href: '/documentation/tutorial/api-fetching' }}
|
||||
/>
|
||||
@@ -0,0 +1,174 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Dynamic routes"
|
||||
canonical="https://tuono.dev/documentation/tutorial/dynamic-routes"
|
||||
description="Learn how to catch more endpoint with the same route loading dynamic data"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Dynamic routes' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Dynamic routes
|
||||
|
||||
## Create the Pokémon route
|
||||
|
||||
The homepage is ready. We have the full list of Pokémon and they are all links. Great!
|
||||
|
||||
Now we want to make those links actually point to a real page. Let’s create the dynamic route.
|
||||
|
||||
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
|
||||
|
||||
These two will handle every request that points to `http://localhost:3000/pokemons/bulbasaur..mew`.
|
||||
|
||||
Let’s first work on the server side file. Paste into the new `[pokemon].rs` file the following code:
|
||||
|
||||
```rust
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use reqwest::Client;
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
id: u16,
|
||||
weight: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||
let pokemon = req.params.get("pokemon").unwrap();
|
||||
|
||||
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemon>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
Err(_err) => Response::Props(Props::new("{}"))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code:
|
||||
|
||||
```tsx
|
||||
import type { JSX } from 'react'
|
||||
import { TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||
return <PokemonView pokemon={data} />
|
||||
}
|
||||
```
|
||||
|
||||
The browser should complain that the component `PokemonView` does not exist. Let’s create it then!
|
||||
|
||||
```tsx
|
||||
// components/PokemonView.tsx
|
||||
import { Link } from 'tuono'
|
||||
import styles from './PokemonView.module.css'
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
id: string
|
||||
weight: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export default function PokemonView({
|
||||
pokemon,
|
||||
}: {
|
||||
pokemon?: Pokemon
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<Link className={styles['back-btn']} href="/">
|
||||
Back
|
||||
</Link>
|
||||
{pokemon?.name && (
|
||||
<div className={styles.pokemon}>
|
||||
<div>
|
||||
<h1 className={styles.name}>{pokemon.name}</h1>
|
||||
<dl className={styles.spec}>
|
||||
<dt className={styles.label}>Weight:</dt>
|
||||
<dd>{pokemon.weight}lbs</dd>
|
||||
</dl>
|
||||
<dl className={styles.spec}>
|
||||
<dt className={styles.label}>Height:</dt>
|
||||
<dd>{pokemon.height}ft</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<img
|
||||
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
```css
|
||||
/* components/PokemonView.module.css */
|
||||
.back-btn {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
padding: 7px 15px;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
border: solid #f0f0f0 1px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
||||
}
|
||||
|
||||
.pokemon {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 50px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pokemon img {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.spec {
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 700;
|
||||
}
|
||||
```
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{ title: 'Components', href: '/documentation/tutorial/components' }}
|
||||
next={{
|
||||
title: 'Error handling',
|
||||
href: '/documentation/tutorial/error-handling',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,116 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Error handling"
|
||||
canonical="https://tuono.dev/documentation/tutorial/error-handling"
|
||||
description="Learn how to handle the server side error on your Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Error handling' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Error handling
|
||||
|
||||
With the current setup, all the routes always return a `200 Success` HTTP status, no matter the response type.
|
||||
|
||||
In order to return a more meaningful status code to the browser, the `Props` struct can also be initialized with the
|
||||
`Props::new_with_status()` method.
|
||||
|
||||
Let's see how it works!
|
||||
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
-- use reqwest::Client;
|
||||
++ use reqwest::{Client, StatusCode};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
id: u16,
|
||||
weight: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||
let pokemon = req.params.get("pokemon").unwrap();
|
||||
|
||||
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
|
||||
Ok(res) => {
|
||||
++ if res.status() == StatusCode::NOT_FOUND {
|
||||
++ return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND));
|
||||
++ }
|
||||
|
||||
let data = res.json::<Pokemon>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
-- Err(_err) => Response::Props(Props::new(
|
||||
++ Err(_err) => Response::Props(Props::new_with_status(
|
||||
++ "{}",
|
||||
++ StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
```diff
|
||||
// src/routes/index.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
-- use reqwest::Client;
|
||||
++ use reqwest::{Client, StatusCode};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemons {
|
||||
results: Vec<Pokemon>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Pokemon {
|
||||
name: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||
return match fetch.get(ALL_POKEMON).send().await {
|
||||
Ok(res) => {
|
||||
let data = res.json::<Pokemons>().await.unwrap();
|
||||
Response::Props(Props::new(data))
|
||||
}
|
||||
-- Err(_err) => Response::Props(Props::new(
|
||||
++ Err(_err) => Response::Props(Props::new_with_status(
|
||||
++ "{}", // Return empty JSON
|
||||
++ StatusCode::INTERNAL_SERVER_ERROR,
|
||||
)),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If you now try to load a not-existing Pokémon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
|
||||
correctly receive a 404 status code in the console.
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{
|
||||
title: 'Dynamic routes',
|
||||
href: '/documentation/tutorial/dynamic-routes',
|
||||
}}
|
||||
next={{ title: 'SEO and meta tags', href: '/documentation/tutorial/seo' }}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Tutorial"
|
||||
canonical="https://tuono.dev/documentation/tutorial"
|
||||
description="This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the development - be sure to have installed the latest version"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
|
||||
|
||||
# Tutorial
|
||||
|
||||
This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the
|
||||
development - be sure to have [installed](/documentation/installation) the [latest version](https://crates.io/crates/tuono).
|
||||
|
||||
This tutorial is not meant for people who don't know React - in that case, I suggest you first read the
|
||||
[React Doc](https://react.dev/); Typescript and Rust knowledge is not a requirement though!
|
||||
|
||||
If you prefer to just read the code rather than write you can download the finished tutorial project with:
|
||||
|
||||
```bash
|
||||
$ tuono new tutorial --template tutorial
|
||||
```
|
||||
|
||||
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it)
|
||||
> or on Twitter (X) DM [@valerioageno](https://twitter.com/valerioageno)
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
next={{
|
||||
title: 'Development setup',
|
||||
href: '/documentation/tutorial/development-setup',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,47 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Building for production"
|
||||
canonical="https://tuono.dev/documentation/tutorial/production"
|
||||
description="Learn how to build the production project and run the optimize version"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Production build' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Production build
|
||||
|
||||
The source is now ready to be released. Both server and client have been managed in an unoptimized way
|
||||
to ease the development experience. To build the project to the production state, just run:
|
||||
|
||||
```shell
|
||||
$ tuono build
|
||||
```
|
||||
|
||||
This command just created the final assets within the `out` directory. To run the production server,
|
||||
run:
|
||||
|
||||
```shell
|
||||
$ cargo run --release
|
||||
```
|
||||
|
||||
Check again [`http://localhost:3000/`](http://localhost:3000/) This environment now has all the
|
||||
optimizations ready to unleash the power of a rust server that seamlessly renders a React application!🚀
|
||||
|
||||
> Note: The `out` directory is not standalone. You can't rely just on it to run the production server.
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{
|
||||
title: 'Server redirection',
|
||||
href: '/documentation/tutorial/redirections',
|
||||
}}
|
||||
next={{ title: 'Conclusion', href: '/documentation/tutorial/conclusion' }}
|
||||
/>
|
||||
@@ -0,0 +1,61 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Redirection"
|
||||
canonical="https://tuono.dev/documentation/tutorial/redirections"
|
||||
description="Learn how to redirect to different route from the server"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Redirections' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Redirections
|
||||
|
||||
What if there is a Pokémon among all of them that should be considered the GOAT? What
|
||||
we are going to do right now is creating a new route `/pokemons/GOAT` that points to the best
|
||||
Pokémon of the first generation.
|
||||
|
||||
First, let's create a new route by just creating a new file `/pokemons/GOAT.rs` and pasting the following code:
|
||||
|
||||
```rs
|
||||
// src/routes/pokemons/GOAT.rs
|
||||
use tuono_lib::{Request, Response};
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn redirect_to_goat(_req: Request) -> Response {
|
||||
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
|
||||
Response::Redirect("/pokemons/mewtwo".to_string())
|
||||
}
|
||||
```
|
||||
|
||||
Now let's create the button in the home page to actually point to it!
|
||||
|
||||
```diff
|
||||
// src/routes/index.tsx
|
||||
|
||||
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
|
||||
++ <PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
|
||||
{data.results.map((pokemon, i) => {
|
||||
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
|
||||
})}
|
||||
</ul>
|
||||
```
|
||||
|
||||
Now at [http://localhost:3000/](http:/localhost:3000/) You will find a new link at the beginning of the list.
|
||||
Click on it and see the application automatically redirecting you to your favourite pokemon's route!
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{ title: 'SEO and meta tags', href: '/documentation/tutorial/seo' }}
|
||||
next={{
|
||||
title: 'Production build',
|
||||
href: '/documentation/tutorial/production',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,115 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - SEO and meta tags"
|
||||
canonical="https://tuono.dev/documentation/tutorial/seo"
|
||||
description="Learn how to handle meaningful meta tags for your Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'SEO and meta tags' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# SEO and meta tags
|
||||
|
||||
The website now works and the HTTP errors are meaningful, but we should also take care to be meaningful
|
||||
for the web crawlers. The best way to do it is to enrich the meta tags like the `<title>` and the
|
||||
`<description>`.
|
||||
|
||||
To do so `tuono` also exposes the `<Head />` component useful exactly for handling this scenario. Let's update the `/` and the
|
||||
`/pokemons/[pokemon]` routes with this.
|
||||
|
||||
```diff
|
||||
// src/routes/index.tsx
|
||||
import type { JSX } from 'react'
|
||||
import type { TuonoProps } from "tuono";
|
||||
++ import { Head } from "tuono"
|
||||
|
||||
interface Pokemon {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface IndexProps {
|
||||
results: Pokemon[]
|
||||
}
|
||||
|
||||
export default function IndexPage({
|
||||
data,
|
||||
}: TuonoProps<IndexProps>): JSX.Element {
|
||||
if (!data?.results) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
++ <Head>
|
||||
++ <title>Tuono tutorial</title>
|
||||
++ </Head>
|
||||
<header className="header">
|
||||
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||
Crates
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/tuono" target="_blank">
|
||||
Npm
|
||||
</a>
|
||||
</header>
|
||||
<div className="title-wrap">
|
||||
<h1 className="title">
|
||||
TU<span>O</span>NO
|
||||
</h1>
|
||||
<div className="logo">
|
||||
<img src="rust.svg" className="rust" />
|
||||
<img src="react.svg" className="react" />
|
||||
</div>
|
||||
</div>
|
||||
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
|
||||
{data.results.map((pokemon) => {
|
||||
return pokemon.name;
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].tsx
|
||||
import type { JSX } from 'react'
|
||||
-- import { TuonoProps } from "tuono";
|
||||
++ import { TuonoProps, Head } from "tuono";
|
||||
import PokemonView from "../../components/PokemonView";
|
||||
|
||||
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||
-- return <PokemonView pokemon={data} />;
|
||||
++ return (
|
||||
++ <>
|
||||
++ <Head>
|
||||
++ <title>Pokemon: ${data?.name}</title>
|
||||
++ </Head>
|
||||
++ <PokemonView pokemon={data} />
|
||||
++ </>
|
||||
++ )
|
||||
}
|
||||
```
|
||||
|
||||
The `Head` component takes as children any valid HTML meta tag.
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
prev={{
|
||||
title: 'Error handling',
|
||||
href: '/documentation/tutorial/error-handling',
|
||||
}}
|
||||
next={{
|
||||
title: 'Server redirection',
|
||||
href: '/documentation/tutorial/redirections',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import Hero from '../components/hero'
|
||||
import MetaTags from '../components/meta-tags'
|
||||
|
||||
export default function IndexPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<MetaTags
|
||||
title="Tuono - The React/Rust full-stack framework"
|
||||
canonical="https://tuono.dev"
|
||||
description="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
|
||||
/>
|
||||
<Hero />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use tuono_lib::reqwest::StatusCode;
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::axum::http::{header, HeaderMap};
|
||||
use glob::glob;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
const FILE_TO_EXCLUDE: [&str; 2] = ["sitemap.xml", "__root"];
|
||||
|
||||
const SITEMAP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
[PLACEHOLDER]
|
||||
</urlset>"#;
|
||||
|
||||
fn load_routes() -> Vec<String> {
|
||||
let mut paths: Vec<String> = vec![];
|
||||
|
||||
for entry in glob("./src/routes/**/*").expect("Failed to glob src/routes folder").flatten() {
|
||||
if !entry.is_dir() {
|
||||
let path = clean_path(format!("/{}", entry.to_string_lossy()));
|
||||
|
||||
if !FILE_TO_EXCLUDE.iter().any(|exclude| path.ends_with(exclude)) {
|
||||
paths.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
fn clean_path(value: String) -> String {
|
||||
value
|
||||
.replace("src/routes/", "")
|
||||
.replace(".mdx", "")
|
||||
.replace(".tsx", "")
|
||||
.replace(".rs", "")
|
||||
.replace("index", "")
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn generate_sitemap(_req: Request) -> Response {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::CONTENT_TYPE, "text/xml".parse().unwrap());
|
||||
|
||||
let routes = load_routes();
|
||||
|
||||
let mut sitemaps = String::new();
|
||||
|
||||
for path in routes {
|
||||
let mut url = format!("https://tuono.dev{}", path);
|
||||
|
||||
if url.ends_with('/') {
|
||||
url.pop();
|
||||
}
|
||||
|
||||
sitemaps.push_str(
|
||||
&format!(r#"<url><loc>{}</loc><lastmod>{}</lastmod></url>"#,url, OffsetDateTime::now_utc().date())
|
||||
)
|
||||
}
|
||||
|
||||
Response::Custom((StatusCode::OK, headers, SITEMAP.replace("[PLACEHOLDER]", &sitemaps)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||