Compare commits

...

61 Commits

Author SHA1 Message Date
Valerio Ageno 209c9fb7f7 feat: update version to v0.13.2 2024-11-19 17:34:33 +01:00
Valerio Ageno a562f67e8d Feat: added discord to README 2024-11-19 16:43:58 +01:00
Valerio Ageno ef0fe5a425 fix: seo page canonical 2024-11-19 15:50:58 +01:00
Myan V. b50902d23e Update watchexec to v5 and watchexec-signals to v4 (#118)
* fix(deps): update rust crate watchexec to v5

* fix(deps): update rust crate watchexec-signals to v4

* fix: revert unnecessary imports
2024-11-19 14:32:44 +01:00
Marco Pasqualetti 55490579ec refactor: replace () => JSX.Element with React.ComponentType (#120) 2024-11-18 21:45:27 +01:00
renovate[bot] 957d08701f chore(deps): update typescript-eslint monorepo to v8.15.0 (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 20:41:04 +01:00
Valerio Ageno 61e1aed35d Add not expiring discord invite to documentation website 2024-11-18 18:36:33 +01:00
Valerio Ageno 2034f0ef5f fix: tutorial/redirections canonical tag 2024-11-18 12:35:14 +01:00
Valerio Ageno fcb92c7f0b fix(doc): spelling 2024-11-17 18:24:56 +01:00
Marco Pasqualetti 28ea1ca1df ci: add format check on repository root (#115) 2024-11-17 17:51:20 +01:00
Valerio Ageno ebc1a458a9 Fix tuono_lib_macro crate (#116)
* fix: tuono lib macro

* feat: remove reqwest re-export

* feat: update version to 0.13.1

* fix: remove reqwest from documentation
2024-11-17 17:11:07 +01:00
Valerio Ageno df92ef5296 Add support for custom global state (#111)
* feat: add 'has_main_file' to App struct

* feat: handle custom state from the main.rs file

* feat: update tutorial template with new setup

* fix: apply clippy hints

* feat: add ApplicationState mention to documentation website

* feat: update version to v0.13.0
2024-11-17 16:28:56 +01:00
Valerio Ageno 253e0a8378 feat(doc): keep social buttons on mobile website 2024-11-17 12:26:58 +01:00
Valerio Ageno a9fbe4eddd feat: add discord channel invite to documentation website 2024-11-17 12:19:25 +01:00
Valerio Ageno cfccc638c6 feat: update version to v0.12.4 2024-11-16 19:23:21 +01:00
renovate[bot] 9f1460cc26 fix(deps): update mantine monorepo to v7.14.1 (#110) 2024-11-16 17:11:02 +01:00
Henry Chen f0a4541420 fix: handle hyphen in route (#109) 2024-11-16 17:10:37 +01:00
renovate[bot] d0ce482680 chore(deps): update dependency vitest to v2.1.5 (#105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:29:50 +01:00
renovate[bot] 40e2734a4e fix(deps): update dependency turbo to v2.3.0 (#106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:28:58 +01:00
Marco Pasqualetti 8552796343 chore: fix typo and improve name on CI (#107) 2024-11-16 11:47:53 +01:00
Marco Pasqualetti 6d72ffb876 ci: create install-node-dependencies action (#100) 2024-11-16 11:27:10 +01:00
Valerio Ageno 65faaf0183 feat: added website to crates/package manifest 2024-11-16 11:08:50 +01:00
Valerio Ageno 7f7c2e8d6f feat: update documentation tuono to v0.12.3 (#104) 2024-11-16 11:02:47 +01:00
Valerio Ageno 3be491baa7 Fix tuono lib release (#103)
* fix: removed service

* feat: update version to v0.12.3
2024-11-16 10:50:02 +01:00
Valerio Ageno 48b974ae58 Fix failing release pipeline (#102)
* feat: add no-verify key to cargo publish script

* feat: update version to v0.12.2
2024-11-16 10:23:59 +01:00
Valerio Ageno c394898fcd Create logger service (#101)
* feat: create logger service

* feat: opt out __tuono/data routes from logger

* feat: update version to v0.12.1
2024-11-16 09:47:59 +01:00
renovate[bot] f22249f3e9 chore(deps): update pnpm to v9.13.2 (#99) 2024-11-15 07:08:49 +01:00
renovate[bot] 120cec52e9 chore(deps): update pnpm to v9.13.1 (#98)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 20:53:19 +01:00
Marco Pasqualetti 535cc6b3d9 chore: add apps/documentation to pnpm workspaces (#96) 2024-11-14 10:30:21 +01:00
renovate[bot] 18a060e2bf chore(deps): update pnpm to v9.13.0 (#94)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 16:55:37 +01:00
Marco Pasqualetti 7b8165cee6 refactor: use React.JSX rather than global JSX (#93) 2024-11-13 08:33:48 +01:00
Valerio Ageno 6331eb64e6 fix: development script watch loop (#92)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-12 14:21:16 +01:00
Marco Pasqualetti 3c38b551f0 docs(CONTRIBUTING): new link issue point to issue template chooser (#90) 2024-11-12 13:46:34 +01:00
renovate[bot] 8e761347f2 fix(deps): update rust crate tuono_lib to 0.12.0 (#89)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 08:45:21 +01:00
Valerio Ageno e2d44e0e6a fix: format documentation 2024-11-11 21:56:22 +01:00
Valerio Ageno 607da92c71 Make handler arguments optional (#87)
* feat: support axum extractors

* chore: update tuono example

* chore: update tutorial example

* doc: update documentation

* feat: update version to v0.12.0

* fix: use clippy suggestion
2024-11-11 21:52:22 +01:00
Henry Chen ae0427604e Fix tutorial link in README (#86) 2024-11-11 13:32:56 +01:00
Marco Pasqualetti 2aa981c581 chore: use GitHub forms in issue template (#85)
* chore: use GitHub forms in issue template

* Update .github/ISSUE_TEMPLATE/bug_report.yml

---------

Co-authored-by: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com>
2024-11-11 09:28:25 +01:00
Valerio Ageno 5d8ec82502 feat: add log to catch_all routes (#83) 2024-11-10 11:08:06 +01:00
Valerio Ageno fc9bb52c41 Update getting started (#82)
* feat: update getting started doc

* feat: update README
2024-11-10 10:44:36 +01:00
Valerio Ageno 3224e4138f feat: update doc hero 2024-11-10 09:52:51 +01:00
Valerio Ageno 7ea28c4fd3 fix: documentation typo 2024-11-10 09:50:47 +01:00
Valerio Ageno acb5567f6a fix: breadcrumbs structured data 2024-11-09 19:27:02 +01:00
Valerio Ageno cc877c7c11 fix: doc formatting 2024-11-06 21:11:57 +01:00
Valerio Ageno 87bdddf7cf feat: add twitter OG 2024-11-06 21:11:32 +01:00
Valerio Ageno d939b64d64 fix: documentation meta breadcrumb 2024-11-06 21:09:30 +01:00
Valerio Ageno 3f64f71947 feat: update documentation version to v0.11.2 (#81)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-05 12:13:30 +01:00
Valerio Ageno aaae7db24d Improve route preloading (#80)
* feat: improve route preloading

* feat: update version to v0.11.2

---------

Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-05 12:03:20 +01:00
Valerio Ageno 48c3934b6e Add seo tags to documentation (#78)
* refactor: update documentation meta tags

* feat: update documentation version to v0.11.1

* fix: formatting

* feat: add SEO breadcrumb structured data
2024-11-04 21:23:56 +01:00
Valerio Ageno 258b4af0b1 feat: prevent scroll offset on page navigation (#77) 2024-11-04 19:29:37 +01:00
Valerio Ageno edfbd03f13 Prevent not exported routes error (#76)
* fix: prevent no exported route compiler error

* feat: update version to v0.11.1

* fix: formatting
2024-11-04 19:25:46 +01:00
Valerio Ageno 9cb0f62da7 fix: documentation server/client mismatch 2024-11-04 17:15:37 +01:00
Valerio Ageno 9591960e31 feat: add canonical tags to documentation 2024-11-03 22:34:23 +01:00
Valerio Ageno 132d8ebe8f feat: add navigation buttons to tutorial 2024-11-03 22:34:12 +01:00
Valerio Ageno 4020073aca feat: update doc tuono version to v0.11.0 2024-11-03 22:34:00 +01:00
Valerio Ageno 5025692220 fix: put mantine color scheme component outside Head 2024-11-03 22:27:17 +01:00
Valerio Ageno d04a4c4103 Move to react-helmet-async (#72)
* feat: move to react-helmet-async

* feat: set version to v0.11.0
2024-11-03 21:09:32 +01:00
renovate[bot] 7e6dea68f4 chore(deps): update pnpm to v9.12.3 (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 19:02:59 +01:00
renovate[bot] a046833414 chore(deps): update dependency node to v22 (#71)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 19:02:43 +01:00
renovate[bot] cfa59ca8bf fix(deps): update dependency @types/node to v22 (#70) 2024-10-29 07:02:39 +01:00
renovate[bot] d470bd58f2 doc: update tuono version to v0.10.4 (#61)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-10-18 15:22:54 +02:00
128 changed files with 8728 additions and 616 deletions
+2 -1
View File
@@ -49,7 +49,7 @@
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
@@ -59,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",
-23
View File
@@ -1,23 +0,0 @@
## Description
A clear and concise description of what the bug is.
## Expected behaviour
A clear and concise description of what you expected to happen.
## How to reproduce
Steps to reproduce the behavior:
## Screenshots
If applicable, add screenshots to help explain your problem.
## Tooling and environment
- Tuono version [e.g. 0.4.0]
- OS: [e.g. MacOS, Windows]
- Browser: [e.g. chrome, safari]
- Node version: [e.g. 20.0.0]
- Rust version: [e.g. 1.79.0]
- Crate version: [e.g. 1.78.0]
- Node Package Manger version: [e.g. pnpm: 9.5.0]
## Additional context
Add any other context about the problem here.
+90
View File
@@ -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
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
## Context & Description
<!--
Thank you for your Pull Request.
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
+32 -50
View File
@@ -13,63 +13,45 @@ on:
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
defaults:
run:
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
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.3
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
- name: Build project
run: tuono build --static
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
defaults:
run:
working-directory: ./apps/documentation
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
- 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
+8 -19
View File
@@ -10,36 +10,25 @@ on:
jobs:
deploy-documentation:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./apps/documentation
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Check out code
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.3
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
run: cargo install tuono@0.12.3
- name: Build project
working-directory: ./apps/documentation
run: tuono build --static
- name: Deploy
+23 -37
View File
@@ -7,42 +7,35 @@ jobs:
publish-on-npm-dry-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Publish
run: pnpm publish -r --no-git-checks --dry-run
run: pnpm publish -r --no-git-checks --dry-run
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-on-crates-dry-run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
dry-run: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
@@ -52,27 +45,17 @@ jobs:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Clone root README.md on tuono package
run: cp README.md packages/tuono/README.md
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
@@ -85,13 +68,16 @@ jobs:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
check-repo: true
+31
View File
@@ -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
+8 -3
View File
@@ -10,16 +10,19 @@ on:
- '.github/**'
- 'crates/**'
env:
env:
CARGO_TERM_COLOR: always
jobs:
lint_and_fmt:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check format and lint rust crates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- run: cargo fmt --all -- --check
- run: cargo clippy -- -D warnings
@@ -34,7 +37,9 @@ jobs:
- beta
- nightly
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
+3 -1
View File
@@ -7,7 +7,9 @@ jobs:
name: runner / LanguageTools
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
+26 -39
View File
@@ -13,54 +13,41 @@ on:
jobs:
build-and-test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check build and test
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build project
run: pnpm build
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Test project
run: pnpm test
- name: Build project
run: pnpm build
- name: Test project
run: pnpm test
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check format and lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
- 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
+3 -3
View File
@@ -1,12 +1,12 @@
## Node related ignores
node_modules/
dist/
.turbo/
*/**/routeTree.gen.ts
pnpm-lock.yaml
examples/playground/
examples/*/pnpm-lock.yaml
## Rust related ignores
+1 -1
View File
@@ -1 +1 @@
20.18.0
22.11.0
+1
View File
@@ -0,0 +1 @@
pnpm-lock.yaml
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"trailingComma": "all"
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
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,
- 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
- 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
- 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
- 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
- Other conduct that could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ 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
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
+6 -3
View File
@@ -4,18 +4,21 @@
# How to Contribute to Tuono
## Contributions
## 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)
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). 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.
- 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
+22 -12
View File
@@ -1,5 +1,5 @@
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
[✨Contributing](https://tuono.dev/documentation/contributing)
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
# Tuono
@@ -10,16 +10,18 @@
**Tuono is the fastest React framework out there**
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
Some of its features are:
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
- 🟦 Native Typescript
- 🌐 NextJs like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
Some of its features are:
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
## 📖 Documentation
@@ -30,7 +32,7 @@ The [documentation](https://tuono.dev/documentation) is available on
**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.
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.
@@ -40,12 +42,19 @@ Tuono provides a collection of utilities to handle the server side code seamless
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
As already mentioned above, I strongly suggest you take a look at the
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
[tutorial](https://tuono.dev/documentation/tutorial).
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
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.
@@ -53,7 +62,7 @@ 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
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
Then, to run the local development environment, run it inside the project folder `tuono dev`
@@ -64,6 +73,7 @@ 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
+2 -1
View File
@@ -49,7 +49,7 @@
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
@@ -59,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",
+1 -1
View File
@@ -8,7 +8,7 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.10.3"
tuono_lib = "0.12.3"
glob = "0.3.1"
time = { version = "0.3", features = ["macros"] }
serde = { version = "1.0.202", features = ["derive"] }
+5 -14
View File
@@ -1,7 +1,8 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"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/**/*'",
@@ -17,24 +18,14 @@
"clsx": "^2.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "0.10.3"
"tuono": "npm:tuono@0.12.3"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8.4.39",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.4",
"typescript": "^5.5.4"
"postcss-simple-vars": "^7.0.1"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

@@ -1,31 +1,73 @@
import type { JSX } from 'react'
import { Breadcrumbs, Button } from '@mantine/core'
import { Link } from 'tuono'
import { Link, Head } from 'tuono'
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
import type { ReactNode } from 'react'
interface Breadcrumb {
href?: string
label: string
}
interface BreadcrumbsProps {
breadcrumbs: Breadcrumb[]
}
export default function TuonoBreadcrumbs({
children,
}: {
children: ReactNode
}): JSX.Element {
breadcrumbs = [],
}: BreadcrumbsProps): JSX.Element {
return (
<Breadcrumbs
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
mb="md"
mt="md"
>
<Button
href="/documentation"
component={Link}
variant="subtle"
radius="xl"
p={5}
<>
<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"
>
<IconBolt />
</Button>
{children}
</Breadcrumbs>
<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>
</>
)
}
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Button } from '@mantine/core'
import { IconEdit } from '@tabler/icons-react'
import { useRouter } from 'tuono'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import {
Button,
Center,
@@ -15,7 +16,7 @@ export default function Hero(): JSX.Element {
return (
<Container size={1100} my={100}>
<Center>
<Title order={1}>The React/Rust full-stack framework</Title>
<Title order={1}>Superfast fullstack react framework</Title>
</Center>
<Center mt={50}>
<Text fz="18px">
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Text, type TextProps } from '@mantine/core'
export default function MdxBold(props: TextProps): JSX.Element {
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Code } from '@mantine/core'
import type { HTMLAttributes } from 'react'
@@ -1,4 +1,4 @@
import type { AnchorHTMLAttributes } from 'react'
import type { JSX, AnchorHTMLAttributes } from 'react'
import { Button } from '@mantine/core'
import { Link } from 'tuono'
import { IconExternalLink } from '@tabler/icons-react'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { CodeHighlight } from '@mantine/code-highlight'
import styles from './mdx-pre.module.css'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
import MdxLink from './mdx-link'
@@ -1,5 +1,5 @@
import type { JSX, HTMLAttributes } from 'react'
import { Blockquote, Space } from '@mantine/core'
import type { HTMLAttributes } from 'react'
export default function MdxQuote(
props: HTMLAttributes<HTMLQuoteElement>,
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Title, type TitleProps } from '@mantine/core'
export default function MdxTitle(props: TitleProps): JSX.Element {
@@ -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>
)
}
@@ -1,15 +1,22 @@
import type { JSX } from 'react'
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
import { IconBrandGithub, IconBook, IconBrandX } from '@tabler/icons-react'
import ThemeBtn from '../theme-btn'
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} visibleFrom="sm">
<Group gap={8}>
<Button
href="/documentation"
component={Link}
href="/documentation"
visibleFrom="sm"
size="compact-lg"
rightSection={<IconBook />}
autoContrast
@@ -29,12 +36,12 @@ export default function Actions(): JSX.Element {
<ActionIcon
variant="default"
size="lg"
aria-label="We are also on X!"
href="https://x.com/tuono_dev"
aria-label="Join the Tuono's community on Discord!"
href="https://discord.com/invite/khQzPa654B"
target="_blank"
component="a"
>
<IconBrandX />
<IconBrandDiscord />
</ActionIcon>
</Group>
<ThemeBtn />
@@ -1,5 +1,7 @@
import type { JSX } from 'react'
import { AppShell, Burger, Button, Flex } from '@mantine/core'
import { Link, useRouter } from 'tuono'
import Actions from './actions'
interface NavbarProps {
@@ -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>
)
}
@@ -1,5 +1,6 @@
import type { JSX, ReactNode } from 'react'
import { useState } from 'react'
import { NavLink, type NavLinkProps } from '@mantine/core'
import { useState, type ReactNode } from 'react'
import { Link, useRouter } from 'tuono'
import { IconChevronRight } from '@tabler/icons-react'
@@ -1,10 +1,20 @@
import type { JSX } from 'react'
import { AppShell } from '@mantine/core'
import SidebarLink from './sidebar-link'
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
interface SidebarProps {
close: () => void
}
export default function Sidebar({ close }: SidebarProps): JSX.Element {
return (
<AppShell.Navbar p="md">
<SidebarLink href="/documentation" label="Overview" onClick={close} />
<SidebarLink
href="/documentation"
label="Getting started"
onClick={close}
/>
<SidebarLink
href="/documentation/installation"
label="Installation"
@@ -63,6 +73,12 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
/>
</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"
@@ -1,6 +1,7 @@
/*
* 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'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import {
ActionIcon,
useMantineColorScheme,
+2 -2
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import {
ColorSchemeScript,
createTheme,
@@ -86,8 +86,8 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<ColorSchemeScript />
</Head>
<ColorSchemeScript />
<MantineProvider theme={theme}>
<AppShell
header={{ height: 60 }}
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import { AppShell, Container } from '@mantine/core'
import MdxProvider from '../../components/mdx-provider'
import EditPage from '../../components/edit-page'
@@ -0,0 +1,55 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Application state"
canonical="https://tuono.dev/documentation/application-state"
description="Learn how to add features to your Tuono application"
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
# Application state
The main reason Tuono is fast is that it loads just the features that are needed for the project.
To define them, you need to fill the `ApplicationState` struct in the `./src/main.rs` file, and
they will be automatically available in all the handlers you will define across the application.
```rs
// src/main.rs
#[derive(Clone)]
pub struct ApplicationState {
pub website_name: String,
pub base_path: String
}
pub fn main() -> ApplicationState {
let website_name = "tuono".to_string();
let base_path = "http://localhost:3000".to_string();
ApplicationState {
website_name,
base_path
}
}
```
> For the sake of simplicity here we just enable a `String` but you can add Database connections or
> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client).
Now the `ApplicationState` is available on all the handlers as following:
```rs
// src/routes/index.rs
#[tuono_lib::handler]
// The first argument always is the Request
// The ApplicationState arguments are optional. You can use just the ones you need
// to use in the handler (with no specific order).
async fn my_handler(req: Request, website_name: String) -> Response {
...
}
```
@@ -1,14 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - CLI</title>
</Head>
<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>
<Element label="CLI" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
# CLI
@@ -1,14 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - Contributing</title>
</Head>
<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>
<Element label="✨ Contributing" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
# Contributing
@@ -1,48 +1,54 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - Documentation</title>
</Head>
<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></Breadcrumbs>
<Breadcrumbs />
# Tuono
**Tuono is a full-stack framework for building React applications using Rust as the
backend.**
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.
## 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.
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
## Features
Some of its features include:
- 🟦 Full typescript support
- 🌐 File system based routing
- 🔥 Hot Module Reload
- 🍭 SCSS/CSS modules
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🧵 Multi-threaded backend
- ⌨️ MDX support
- ⚙️ Build optimizations
- Custom APIs\*
- Image optimization\*
- Server streamed content\*
- 🔥 Hot Module Reload
> \*development in progress
## 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)).
@@ -1,13 +1,14 @@
import { Head } from 'tuono'
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Head>
<title>Tuono - Installation</title>
</Head>
import MetaTags from '../../components/meta-tags'
<Breadcrumbs>
<Element label="Installation" />
</Breadcrumbs>
<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
@@ -1,14 +1,13 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Routing</title>
</Head>
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing/intro"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Routing" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
# Routing index
@@ -1,15 +1,18 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Routing</title>
</Head>
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Routing" href="/documentation/routing" />
<Element label="Project structure" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Routing', href: '/documentation/routing' },
{ label: 'Project structure' },
]}
/>
# Project structure
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - API fetching</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="API fetching" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'API fetching' },
]}
/>
# API fetching
@@ -24,7 +28,7 @@ Clear the `index.rs` file and paste:
```rust
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use reqwest::Client;
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
@@ -52,10 +56,80 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
}
```
> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers.
> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional.
The terminal will complain now for two reasons:
1. We don't have imported any `reqwest` crate
2. The second argument `fetch: Client` has not been defined yet as global state.
Let's fix these in the next section.
## Application state
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application.
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
To install it just run in your terminal:
```bash
$ cargo add reqwest
```
A new entry has just been added to your `Cargo.toml` file.
```diff
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.14.0" # the version might be different
serde = { version = "1.0.202", features = ["derive"] }
++ reqwest = "0.12.9" # the version might be different
```
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
> (similarly as the package.json for Javascript).
Now let's define the `ApplicationState` in the `./src/main.rs` file.
```rs
// Import here the just added reqwest library
use reqwest::Client;
#[derive(Clone)]
// Extend this struct with the feature you will need for your application
pub struct ApplicationState {
// This will be available to all your route handlers
pub fetch: Client,
}
pub fn main() -> ApplicationState {
let fetch = Client::new();
return ApplicationState { fetch };
}
```
Now the `fetch: Client` argument is available in the above defined handler, and the terminal should not complain anymore.
Let's see in the next section how to show the fetched data on the browser.
## Handling the page UI
Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste:
```tsx
// src/routes/index.tsx
import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'
interface Pokemon {
@@ -68,9 +142,9 @@ interface IndexProps {
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
}: TuonoProps<IndexProps>): JSX.Element | null {
if (!data?.results) {
return <></>
return null
}
return (
@@ -103,3 +177,13 @@ export default function IndexPage({
```
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' }}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Components</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Components" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Components' },
]}
/>
# Components
@@ -21,6 +25,7 @@ Create the following file `src/components/PokemonLink.tsx` and fill the content
```tsx
// src/components/PokemonLink.tsx
import type { JSX } from 'react'
import { Link } from 'tuono'
interface Pokemon {
@@ -105,8 +110,9 @@ Then import the styles into the `PokemonLink` component as following:
```diff
// src/components/PokemonLink.tsx
import { Link } from "tuono";
import type { Pokemon } from "./../types/pokemon";
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({
@@ -127,3 +133,13 @@ export default function PokemonLink({
);
}
```
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',
}}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Conclusion</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Conclusion" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Conclusion' },
]}
/>
# Conclusion
@@ -22,3 +26,12 @@ As I mentioned in the introduction, I'd love to hear what you thought about the
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',
}}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Development setup</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Development setup" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Development setup' },
]}
/>
# Development setup
@@ -64,3 +68,10 @@ The first time might take a little bit because it will install all the Rusts
> 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' }}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Dynamic routes</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Dynamic routes" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Dynamic routes' },
]}
/>
# Dynamic routes
@@ -17,7 +21,7 @@ import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
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. Lets create the \_\_ route.
Now we want to make those links actually point to a real page. Lets create the dynamic route.
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
@@ -28,7 +32,7 @@ Lets first work on the server side file. Paste into the new `[pokemon].rs` fi
```rust
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use reqwest::Client;
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -59,6 +63,7 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
Then lets 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'
@@ -157,3 +162,13 @@ export default function PokemonView({
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',
}}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Error handling</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Error handling" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Error handling' },
]}
/>
# Error handling
@@ -23,8 +27,8 @@ Let's see how it works!
```diff
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
-- use reqwest::Client;
++ use reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -65,8 +69,8 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
```diff
// src/routes/index.rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
-- 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";
@@ -100,3 +104,13 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
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' }}
/>
@@ -1,14 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Tutorial</title>
</Head>
<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>
<Element label="Tutorial" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
# Tutorial
@@ -26,3 +26,12 @@ $ tuono new tutorial --template tutorial
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it)
> or on Twitter (X) DM [@valerioageno](https://twitter.com/valerioageno)
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
next={{
title: 'Development setup',
href: '/documentation/tutorial/development-setup',
}}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Building for production</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Production build" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Production build' },
]}
/>
# Production build
@@ -31,3 +35,13 @@ Check again [`http://localhost:3000/`](http://localhost:3000/) This environment
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' }}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Redirections</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Redirections" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Redirections' },
]}
/>
# Redirections
@@ -21,10 +25,10 @@ First, let's create a new route by just creating a new file `/pokemons/GOAT.rs`
```rs
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
use tuono_lib::{Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> Response {
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())
}
@@ -45,3 +49,13 @@ Now let's create the button in the home page to actually point to it!
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',
}}
/>
@@ -1,15 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - SEO and meta tags</title>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="SEO and meta tags" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'SEO and meta tags' },
]}
/>
# SEO and meta tags
@@ -22,6 +26,7 @@ To do so `tuono` also exposes the `<Head />` component useful exactly for handli
```diff
// src/routes/index.tsx
import type { JSX } from 'react'
import type { TuonoProps } from "tuono";
++ import { Head } from "tuono"
@@ -76,6 +81,7 @@ export default function IndexPage({
```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";
@@ -94,3 +100,16 @@ export default function Pokemon({ data }: TuonoProps): JSX.Element {
```
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',
}}
/>
+8 -8
View File
@@ -1,16 +1,16 @@
import { Head } from 'tuono'
import type { JSX } from 'react'
import Hero from '../components/hero'
import MetaTags from '../components/meta-tags'
export default function IndexPage(): JSX.Element {
return (
<>
<Head>
<title>Tuono - The React/Rust full-stack framework</title>
<meta
name="description"
content="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
/>
</Head>
<MetaTags
title="Tuono - The React/Rust full-stack framework"
canonical="https://tuono.dev"
description="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
/>
<Hero />
</>
)
+2 -2
View File
@@ -1,4 +1,4 @@
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::reqwest::StatusCode;
use tuono_lib::{Request, Response};
use tuono_lib::axum::http::{header, HeaderMap};
use glob::glob;
@@ -36,7 +36,7 @@ fn clean_path(value: String) -> String {
}
#[tuono_lib::handler]
async fn generate_sitemap(_req: Request, _fetch: Client) -> Response {
async fn generate_sitemap(_req: Request) -> Response {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/xml".parse().unwrap());
+5 -4
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono"
version = "0.10.4"
version = "0.13.2"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
@@ -20,9 +21,9 @@ path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive", "cargo"] }
watchexec = "4.0.0"
watchexec = "5.0.0"
miette = "7.2.0"
watchexec-signals = "3.0.0"
watchexec-signals = "4.0.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
watchexec-supervisor = "3.0.0"
+21 -1
View File
@@ -1,6 +1,9 @@
use glob::glob;
use glob::GlobError;
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
@@ -15,6 +18,15 @@ const IGNORE_FILES: [&str; 1] = ["__root"];
pub struct App {
pub route_map: HashMap<String, Route>,
pub base_path: PathBuf,
pub has_main_file: bool,
}
fn has_main_file(base_path: PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/main.rs"))?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Ok(contents.contains("pub fn main"))
}
impl App {
@@ -23,7 +35,8 @@ impl App {
let mut app = App {
route_map: HashMap::new(),
base_path,
base_path: base_path.clone(),
has_main_file: has_main_file(base_path).unwrap_or(false),
};
app.collect_routes();
@@ -119,6 +132,8 @@ mod tests {
"/home/user/Documents/tuono/src/routes/index.rs",
"/home/user/Documents/tuono/src/routes/posts/index.rs",
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
"/home/user/Documents/tuono/src/routes/posts/handle-this.rs",
"/home/user/Documents/tuono/src/routes/posts/handle-this/[post].rs",
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
"/home/user/Documents/tuono/src/routes/sitemap.xml.rs",
];
@@ -132,6 +147,11 @@ mod tests {
("/about", "about"),
("/posts/index", "posts_index"),
("/posts/[post]", "posts_dyn_post"),
("/posts/handle-this", "posts_handle_hyphen_this"),
(
"/posts/handle-this/[post]",
"posts_handle_hyphen_this_dyn_post",
),
("/posts/UPPERCASE", "posts_uppercase"),
("/sitemap.xml", "sitemap_dot_xml"),
];
+2
View File
@@ -32,6 +32,7 @@ impl AxumInfo {
.to_string()
.replace('/', "_")
.replace('.', "_dot_")
.replace('-', "_hyphen_")
.to_lowercase();
if axum_route.is_empty() {
@@ -47,6 +48,7 @@ impl AxumInfo {
.as_str()
.to_string()
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace(']', ""),
axum_route: axum_route.replace('[', ":").replace(']', ""),
+33 -2
View File
@@ -42,12 +42,17 @@ const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
//MAIN_FILE_IMPORT//
#[tokio::main]
async fn main() {
println!("\n ⚡ Tuono v/*VERSION*/");
//MAIN_FILE_DEFINITION//
let router = Router::new()
// ROUTE_BUILDER
;
//MAIN_FILE_USAGE//;
Server::init(router, MODE).start().await
}
@@ -131,7 +136,33 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
&create_modules_declaration(&app.route_map),
)
.replace("/*VERSION*/", crate_version!())
.replace("/*MODE*/", mode.as_str());
.replace("/*MODE*/", mode.as_str())
.replace(
"//MAIN_FILE_IMPORT//",
if app.has_main_file {
r#"#[path="../src/main.rs"]
mod tuono_main_state;
"#
} else {
""
},
)
.replace(
"//MAIN_FILE_DEFINITION//",
if app.has_main_file {
"let user_custom_state = tuono_main_state::main();"
} else {
""
},
)
.replace(
"//MAIN_FILE_USAGE//",
if app.has_main_file {
".with_state(user_custom_state)"
} else {
""
},
);
let has_server_handlers = app
.route_map
+7 -6
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono_lib"
version = "0.10.4"
version = "0.13.2"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
@@ -14,9 +15,6 @@ include = [
"Cargo.toml"
]
[lib]
name = "tuono_lib"
path = "src/lib.rs"
[dependencies]
ssr_rs = "0.7.0"
@@ -33,9 +31,12 @@ either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.10.4"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.13.2"}
# Match the same version used by axum
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tungstenite = "0.24.0"
http = "1.1.0"
pin-project = "1.1.7"
tower = "0.5.1"
+1 -1
View File
@@ -1,4 +1,5 @@
mod catch_all;
mod logger;
mod manifest;
mod mode;
mod payload;
@@ -18,5 +19,4 @@ pub use tuono_lib_macros::handler;
// Re-exports
pub use axum;
pub use reqwest;
pub use tokio;
+106
View File
@@ -0,0 +1,106 @@
use colored::Colorize;
use http::{method::Method, Request, Response};
use pin_project::pin_project;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::time::Instant;
use tower::{Layer, Service};
#[derive(Clone)]
pub struct LoggerLayer {}
impl LoggerLayer {
pub fn new() -> Self {
LoggerLayer {}
}
}
impl<S> Layer<S> for LoggerLayer {
type Service = Logger<S>;
fn layer(&self, inner: S) -> Self::Service {
Logger::new(inner)
}
}
#[derive(Clone)]
pub struct Logger<S> {
inner: S,
}
impl<S> Logger<S> {
pub fn new(inner: S) -> Self {
Logger { inner }
}
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Logger<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Default,
<S as Service<Request<ReqBody>>>::Error: Debug,
{
type Response = S::Response;
type Error = S::Error;
type Future = LoggerFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let method = req.method().clone();
let path = req.uri().path().to_string();
LoggerFuture {
future: self.inner.call(req),
method,
path,
start: Instant::now(),
}
}
}
#[pin_project]
pub struct LoggerFuture<F> {
#[pin]
future: F,
method: Method,
path: String,
start: Instant,
}
impl<F, B, E> Future for LoggerFuture<F>
where
F: Future<Output = Result<Response<B>, E>>,
B: Default,
E: Debug,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res: F::Output = match this.future.poll(cx) {
Poll::Ready(res) => res,
Poll::Pending => return Poll::Pending,
};
if this.path.starts_with("/__tuono/data") {
return Poll::Ready(res);
}
let status_code = res.as_ref().unwrap().status();
println!(
" {} {} {} in {}ms",
this.method,
this.path,
status_code.as_str().green(),
this.start.elapsed().as_millis()
);
Poll::Ready(res)
}
}
+2 -16
View File
@@ -3,9 +3,7 @@ use crate::{ssr::Js, Payload};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use axum::Json;
use colored::*;
use erased_serde::Serialize;
use tokio::time::Instant;
pub struct Props {
data: Box<dyn Serialize>,
@@ -74,25 +72,13 @@ impl Response {
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
match self {
Self::Props(Props { data, http_code }) => {
let start = Instant::now();
let payload = Payload::new(&req, data).client_payload().unwrap();
let response = match Js::render_to_string(Some(&payload)) {
match Js::render_to_string(Some(&payload)) {
Ok(html) => (*http_code, Html(html)),
Err(_) => (*http_code, Html("500 Internal server error".to_string())),
}
.into_response();
let duration = start.elapsed();
println!(
" GET {} {} in {}ms",
req.uri.path(),
http_code.as_str().green(),
duration.as_millis()
);
response
.into_response()
}
Self::Redirect(to) => Redirect::permanent(to).into_response(),
Self::Custom(response) => response.clone().into_response(),
+14 -11
View File
@@ -1,13 +1,12 @@
use crate::mode::{Mode, GLOBAL_MODE};
use crate::manifest::load_manifest;
use crate::mode::{Mode, GLOBAL_MODE};
use axum::routing::{get, Router};
use colored::Colorize;
use ssr_rs::Ssr;
use tower_http::services::ServeDir;
use crate::{
catch_all::catch_all, vite_reverse_proxy::vite_reverse_proxy,
catch_all::catch_all, logger::LoggerLayer, vite_reverse_proxy::vite_reverse_proxy,
vite_websocket_proxy::vite_websocket_proxy,
};
@@ -15,12 +14,12 @@ const DEV_PUBLIC_DIR: &str = "public";
const PROD_PUBLIC_DIR: &str = "out/client";
pub struct Server {
router: Router<reqwest::Client>,
router: Router,
mode: Mode,
}
impl Server {
pub fn init(router: Router<reqwest::Client>, mode: Mode) -> Server {
pub fn init(router: Router, mode: Mode) -> Server {
Ssr::create_platform();
GLOBAL_MODE.set(mode).unwrap();
@@ -35,17 +34,18 @@ impl Server {
pub async fn start(&self) {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let fetch = reqwest::Client::new();
if self.mode == Mode::Dev {
println!(" Ready at: {}\n", "http://localhost:3000".blue().bold());
let router = self
.router
.to_owned()
.layer(LoggerLayer::new())
.route("/vite-server/", get(vite_websocket_proxy))
.route("/vite-server/*path", get(vite_reverse_proxy))
.fallback_service(ServeDir::new(DEV_PUBLIC_DIR).fallback(get(catch_all)))
.with_state(fetch);
.fallback_service(
ServeDir::new(DEV_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
);
axum::serve(listener, router)
.await
@@ -58,8 +58,11 @@ impl Server {
let router = self
.router
.to_owned()
.fallback_service(ServeDir::new(PROD_PUBLIC_DIR).fallback(get(catch_all)))
.with_state(fetch);
.layer(LoggerLayer::new())
.fallback_service(
ServeDir::new(PROD_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
);
axum::serve(listener, router)
.await
+4 -5
View File
@@ -1,5 +1,5 @@
use axum::body::Body;
use axum::extract::{Path, State};
use axum::extract::Path;
use axum::http::{HeaderName, HeaderValue};
use axum::response::{IntoResponse, Response};
@@ -7,10 +7,9 @@ use reqwest::Client;
const VITE_URL: &str = "http://localhost:3001/vite-server";
pub async fn vite_reverse_proxy(
State(client): State<Client>,
Path(path): Path<String>,
) -> impl IntoResponse {
pub async fn vite_reverse_proxy(Path(path): Path<String>) -> impl IntoResponse {
let client = Client::new();
match client.get(format!("{VITE_URL}/{path}")).send().await {
Ok(res) => {
let mut response_builder = Response::builder().status(res.status().as_u16());
+3 -2
View File
@@ -1,8 +1,9 @@
[package]
name = "tuono_lib_macros"
version = "0.10.4"
version = "0.13.2"
edition = "2021"
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
+85 -15
View File
@@ -1,43 +1,113 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_macro_input, parse_quote, FnArg, ItemFn, Pat, Stmt};
fn create_struct_fn_arg() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
})
.unwrap()
}
fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let local: Stmt = parse_quote!(
use crate::tuono_main_state::ApplicationState;
);
return Some(local);
}
None
}
fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names } = state;);
return Some(use_item);
}
None
}
fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let fn_name = item.clone().sig.ident;
let fn_name = &item.sig.ident;
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
// Fn Arguments minus the first which always is the request
for (i, arg) in item.sig.inputs.iter().enumerate() {
if i == 0 {
axum_arguments.insert(i, params_argument());
continue;
}
if i == 1 {
axum_arguments.insert(1, create_struct_fn_arg())
}
if let FnArg::Typed(pat_type) = arg {
let index = i - 1;
let argument_name = *pat_type.pat.clone();
argument_names.insert(index, argument_name.clone());
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
let application_state_import = import_main_application_state(argument_names.clone());
quote! {
use tuono_lib::axum::response::IntoResponse;
use std::collections::HashMap;
use tuono_lib::axum::extract::{State, Path};
#application_state_import
#item
pub async fn route(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse {
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.render_to_string(req)
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
}
pub async fn api(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse{
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.json()
#fn_name(req.clone(), #argument_names).await.json()
}
}
.into()
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
interface RootRouteProps {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
interface RootRouteProps {
children: ReactNode
+1 -2
View File
@@ -1,5 +1,4 @@
use serde::Serialize;
use tuono_lib::reqwest;
use tuono_lib::{Props, Request, Response};
#[derive(Serialize)]
@@ -8,7 +7,7 @@ struct MyResponse<'a> {
}
#[tuono_lib::handler]
async fn get_server_side_props(_req: Request, _fetch: reqwest::Client) -> Response {
async fn get_server_side_props(_req: Request) -> Response {
Response::Props(Props::new(MyResponse {
subtitle: "The react / rust fullstack framework",
}))
+1
View File
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'
interface IndexProps {
+1
View File
@@ -10,4 +10,5 @@ path = ".tuono/main.rs"
[dependencies]
tuono_lib = { path = "../../crates/tuono_lib/"}
serde = { version = "1.0.202", features = ["derive"] }
reqwest = "0.12.9"
@@ -1,4 +1,6 @@
import type { JSX } from 'react'
import { Link } from 'tuono'
import styles from './PokemonLink.module.css'
interface Pokemon {
+11
View File
@@ -0,0 +1,11 @@
use reqwest::Client;
#[derive(Clone)]
pub struct ApplicationState {
pub fetch: Client,
}
pub fn main() -> ApplicationState {
let fetch = Client::new();
return ApplicationState { fetch };
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
interface RootRouteProps {
children: ReactNode
+1 -1
View File
@@ -1,6 +1,6 @@
// src/routes/index.rs
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
+4 -2
View File
@@ -1,5 +1,7 @@
// src/routes/index.tsx
import type { JSX } from 'react'
import { Head, type TuonoProps } from 'tuono'
import PokemonLink from '../components/PokemonLink'
interface Pokemon {
@@ -12,9 +14,9 @@ interface IndexProps {
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
}: TuonoProps<IndexProps>): JSX.Element | null {
if (!data?.results) {
return <></>
return null
}
return (
@@ -1,7 +1,7 @@
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
use tuono_lib::{Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> Response {
async fn redirect_to_goat(_req: Request) -> Response {
Response::Redirect("/pokemons/mewtwo".to_string())
}
@@ -1,6 +1,6 @@
// src/routes/pokemons/[pokemon].rs
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -1,4 +1,6 @@
import type { JSX } from 'react'
import { Head, type TuonoProps } from 'tuono'
import PokemonView from '../../components/PokemonView'
interface Pokemon {
@@ -14,7 +16,7 @@ export default function PokemonPage({
return (
<>
<Head>
<title>Pokemon: {data?.name}</title>
<title>{`Pokemon: ${data?.name}`}</title>
</Head>
<PokemonView pokemon={data} />
</>
+17 -17
View File
@@ -1,27 +1,27 @@
{
"name": "workspace",
"packageManager": "pnpm@9.1.1",
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
"scripts": {
"dev": "turbo watch dev",
"build": "turbo build --filter tuono",
"lint": "turbo lint",
"format": "turbo format",
"format:check": "turbo format:check",
"types": "turbo types --filter tuono",
"test": "turbo test",
"test:watch": "turbo test:watch"
"dev": "turbo dev --filter=./packages/*",
"build": "turbo build --filter=./packages/*",
"lint": "turbo lint --filter=./packages/*",
"format": "turbo format --filter=./packages/*",
"format:check": "turbo format:check --filter=./packages/*",
"types": "turbo types --filter=./packages/*",
"test": "turbo test --filter=./packages/*",
"test:watch": "turbo test:watch --filter=./packages/*",
"docs:lint": "turbo lint --filter=documentation",
"docs:format": "turbo format --filter=documentation",
"docs:format:check": "turbo format:check --filter=documentation",
"docs:types": "turbo types --filter=documentation",
"repo:root:format": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --write",
"check-all": "turbo build lint format:check types"
},
"workspaces": [
"tuono",
"tuono-lazy-fn-vite-plugin",
"tuono-fs-router-vite-plugin",
"tuono-router"
],
"author": "Valerio Ageno",
"license": "MIT",
"devDependencies": {
"@tanstack/config": "^0.7.0",
"@types/node": "^20.12.7",
"@types/node": "^22.0.0",
"@types/react": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
@@ -38,6 +38,6 @@
"vitest": "^2.0.0"
},
"dependencies": {
"turbo": "^2.0.12"
"turbo": "^2.2.3"
}
}
@@ -0,0 +1,2 @@
dist
pnpm-lock.yaml
+2 -1
View File
@@ -1,7 +1,8 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.10.4",
"version": "0.13.2",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
@@ -21,6 +21,7 @@ import { ROUTES_FOLDER, ROOT_PATH_ID, GENERATED_ROUTE_TREE } from './constants'
import { format } from 'prettier'
import { sortRouteNodes } from './sort-route-nodes'
import isDefaultExported from './utils/is-default-exported'
let latestTask = 0
@@ -50,6 +51,13 @@ async function getRouteNodes(
if (dirent.isDirectory()) {
await recurse(relativePath)
} else if (fullPath.match(/\.(tsx|ts|jsx|js|mdx)$/)) {
// Check that the route is correctly default exported
if (
fullPath.match(/\.(tsx|ts|jsx|js)$/) &&
!isDefaultExported((await fsp.readFile(fullPath)).toString())
) {
return
}
const filePath = replaceBackslash(path.join(dir, dirent.name))
const filePathNoExt = removeExt(filePath)
let routePath =
@@ -0,0 +1,4 @@
export default function isDefaultExported(source: string): boolean {
const regex = new RegExp('export default')
return regex.test(source)
}
@@ -1 +1,2 @@
/** */
// export default
@@ -1 +1 @@
/** */
/** export default */
@@ -1 +1 @@
/** */
/** export default */
@@ -1 +1 @@
/** */
/** export default */

Some files were not shown because too many files have changed in this diff Show More