Compare commits
364 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcb2c91823 | |||
| 9058f209dd | |||
| 1f172d583e | |||
| 439b4757df | |||
| 9f8809bd51 | |||
| 4e326650cc | |||
| e27be27692 | |||
| 88eb2a1897 | |||
| 8c84ef73c8 | |||
| 859e49f16c | |||
| 14ba8b0a23 | |||
| d7c70f75f3 | |||
| 2760e3ea08 | |||
| f2f041fe55 | |||
| 9ec9700ed4 | |||
| c6b047fc02 | |||
| 6727264e39 | |||
| d1a5fe234d | |||
| e84c7cd276 | |||
| 087813fab9 | |||
| 3a4e7398c6 | |||
| 1630fd7e71 | |||
| 4aa99beaff | |||
| eaa0ba28d8 | |||
| 3f3e1120b2 | |||
| 89d7a2c740 | |||
| 56c31f0204 | |||
| 338cfb5be2 | |||
| 43f6fe664b | |||
| 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 | |||
| 9f071d9e13 | |||
| b96fdc5c4e | |||
| 89ffcf0923 | |||
| b91210084f | |||
| cfdd59f5fb | |||
| 5a30f36eb0 | |||
| 322908145e | |||
| ec2fb17f7f | |||
| 9a0bb45a03 | |||
| 04216d93bf | |||
| 4ed6c9902f | |||
| 208c18c94a | |||
| d4c9b25b90 | |||
| 74968b27cc | |||
| db98e0e2fd | |||
| fc1d9013f8 | |||
| da75dc4a07 | |||
| 1ef1a852fc | |||
| aa5d4b689d | |||
| 65f43b3caf | |||
| 6ef526be73 | |||
| c55fe7dde4 | |||
| 6d994a1960 | |||
| b557820b6e | |||
| 4e1d0b6252 | |||
| 90de047a07 | |||
| ef98cfed90 | |||
| b21d2eddc0 | |||
| 4a8a24c708 | |||
| 73c94e5fbb | |||
| df80756c44 | |||
| 3830f18e2d | |||
| 289986b4af |
@@ -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",
|
||||
@@ -97,7 +100,7 @@
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "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: '{.*/**,**}'
|
||||
@@ -1,36 +0,0 @@
|
||||
name: Publish package to npm
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
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 --filter ./packages/tuono
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Publish on npm and crates
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-on-npm-dry-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
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:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
dry-run: true
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
publish-on-npm:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- 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 -r --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- 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:check
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'crates/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'crates/**'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
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: Build and test rust crates
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Spell Checker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
spell_checking:
|
||||
name: LanguageTool
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Keep this value in order to show the error comments
|
||||
# also on edited files.
|
||||
fetch-depth: 2
|
||||
|
||||
# If the workflow is triggered by a push use check reporter
|
||||
#
|
||||
# If the workflow is triggered by a PR coming from the
|
||||
# same repository, the author has write access so we can
|
||||
# use pr-review reporter since the GITHUB_TOKEN will have review
|
||||
# write permissions
|
||||
#
|
||||
# Otherwise use annotation reporter that do not require any
|
||||
# particular permission
|
||||
#
|
||||
# Why this?
|
||||
# https://github.com/Valerioageno/tuono/pull/137#pullrequestreview-2455455272
|
||||
- name: Set reporter
|
||||
id: reporter
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
FROM_MAIN_REPO: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
|
||||
run: |
|
||||
echo "EVENT_NAME: $EVENT_NAME FROM_MAIN_REPO: $FROM_MAIN_REPO"
|
||||
if [ "$EVENT_NAME" == 'push' ]; then
|
||||
echo "name=github-check" >> $GITHUB_OUTPUT
|
||||
elif [ "$FROM_MAIN_REPO" == 'true' ]; then
|
||||
echo "name=github-pr-review" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=github-pr-annotations" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review dog languagetool
|
||||
uses: reviewdog/action-languagetool@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
|
||||
reporter: ${{ steps.reporter.outputs.name }}
|
||||
# Change reporter level if you need.
|
||||
level: 'warning'
|
||||
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,62 @@
|
||||
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
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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,10 +1,26 @@
|
||||
## Node related ignores
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
.turbo/
|
||||
|
||||
*/**/routeTree.gen.ts
|
||||
|
||||
pnpm-lock.yaml
|
||||
examples/*/pnpm-lock.yaml
|
||||
|
||||
examples/playground/
|
||||
## Rust related ignores
|
||||
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
**/debug/
|
||||
**/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
**/Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/tuono",
|
||||
"crates/tuono_lib",
|
||||
"crates/tuono_lib_macros",
|
||||
]
|
||||
exclude = [
|
||||
"apps/documentation",
|
||||
"examples/wit-mdx",
|
||||
"examples/tuono-app",
|
||||
"examples/tuono-tutorial",
|
||||
"benches/tuono"
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2024 Valerio Ageno
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,20 +1,80 @@
|
||||
# Tuono.js
|
||||
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
|
||||
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
|
||||
The fullstack react/rust framework.
|
||||
# Tuono
|
||||
|
||||
> This project is under heavy development. API might drastically change!
|
||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||
## Features
|
||||
- Rust based server side rendering
|
||||
- FS routing
|
||||
- CSS modules
|
||||

|
||||

|
||||
|
||||
## Folder structure
|
||||
**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
|
||||
- public/
|
||||
- src/
|
||||
| - routes/
|
||||
| | - api/
|
||||
```
|
||||
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.
|
||||
|
||||
Some of its features are:
|
||||
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
The [documentation](https://tuono.dev/documentation) is available on
|
||||
[tuono.dev](https://tuono.dev/).
|
||||
|
||||
## Introduction
|
||||
|
||||
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)**
|
||||
|
||||
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.
|
||||
|
||||
**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 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.
|
||||
|
||||
**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
|
||||
|
||||
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, install the JS dependencies (`npm install`) and run 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: 42 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,108 @@
|
||||
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"
|
||||
leftSection="✨"
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/contributing/local-development"
|
||||
label="Local development"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
</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/app.rs` file, and
|
||||
they will be automatically available in all the handlers you will define across the application.
|
||||
|
||||
```rs
|
||||
// src/app.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,57 @@
|
||||
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).
|
||||
|
||||
import NavigationButtons from '../../../components/navigation-buttons'
|
||||
|
||||
<NavigationButtons
|
||||
next={{
|
||||
title: 'Local development',
|
||||
href: '/documentation/contributing/local-development',
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,113 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Contributing - Local development"
|
||||
canonical="https://tuono.dev/documentation/contributing/local-development"
|
||||
description="Contribute to Tuono. Learn here how to setup the repository for local development"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: '✨ Contributing', href: '/documentation/contributing' },
|
||||
{ label: 'Local development' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Local development
|
||||
|
||||
Thanks for your interest! In this page, you find the instructions to set up `tuono` on your local environment!
|
||||
|
||||
## Setup
|
||||
|
||||
### Fork and clone repository
|
||||
|
||||
After [forking the repo on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo):
|
||||
|
||||
```sh
|
||||
git clone https://github.com/<your-name-here>/tuono
|
||||
cd tuono
|
||||
```
|
||||
|
||||
### Rust tool chain
|
||||
|
||||
Install the Rust programming language tool chain (`rust` and `cargo`).
|
||||
Follows instructions in the official [docs](https://rustup.rs/)
|
||||
|
||||
### Node.js — runtime
|
||||
|
||||
Install `Node.js`.
|
||||
You can follow the instructions from the [Node official site](https://nodejs.org/en/download/package-manager)
|
||||
|
||||
> 💡 This project has a `.nvmrc` file to specify the node version used in development.
|
||||
>
|
||||
> Consider to use [nvm](https://github.com/nvm-sh/nvm) so you can run
|
||||
>
|
||||
> ```sh
|
||||
> nvm use
|
||||
> ```
|
||||
>
|
||||
> to simply pick up the correct version!
|
||||
|
||||
### Node.js — package manager
|
||||
|
||||
We use [`pnpm`](https://pnpm.io) as Node.js package manager.
|
||||
|
||||
You can see which version of yarn we use by checking the `packageManager` field in the root `package.json`.
|
||||
|
||||
### Pre-flight checks
|
||||
|
||||
To check that everything is working properly, run:
|
||||
|
||||
```sh
|
||||
turbo run check-all
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Tuono development
|
||||
|
||||
1. Start tuono frontend build using
|
||||
|
||||
```sh
|
||||
turbo run dev
|
||||
```
|
||||
|
||||
2. In another terminal run
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
```
|
||||
|
||||
To automatically rebuild crates on code change, consider using `cargo-watch` crate
|
||||
|
||||
```sh
|
||||
cargo watch -x build -w crates/
|
||||
```
|
||||
|
||||
3. You can now use the binary inside `/target/debug/tuono` in another folder on your local machine
|
||||
|
||||
> Consider adding an alias to your shell setup file
|
||||
>
|
||||
> ```sh
|
||||
> alias t="/path-to-repo/target/debug/tuono"
|
||||
> ```
|
||||
|
||||
## Documentation development
|
||||
|
||||
1. Change the current working directory to the documentation folder:
|
||||
|
||||
```sh
|
||||
cd apps/documentation
|
||||
```
|
||||
|
||||
2. Run
|
||||
|
||||
```sh
|
||||
tuono dev
|
||||
```
|
||||
|
||||
3. Open the localhost URL.
|
||||
|
||||
> On the documentation remember that `tuono` `npm` package is installed from the registry and
|
||||
> it is not linked to the repository.
|
||||
@@ -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/app.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/app.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 tuono-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',
|
||||
}}
|
||||
/>
|
||||