Compare commits

..

123 Commits

Author SHA1 Message Date
Valerio Ageno 8c84ef73c8 feat: update version to v0.14.1 2024-11-24 18:33:45 +01:00
renovate[bot] 859e49f16c fix(deps): update mantine monorepo to v7.14.2 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 18:16:13 +01:00
renovate[bot] 14ba8b0a23 chore(deps): update dependency typescript to v5.7.2 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-23 21:28:03 +01:00
Marco Pasqualetti d7c70f75f3 docs(CONTRIBUTING): add “Local development” (#136) 2024-11-23 21:04:01 +01:00
Valerio Ageno 2760e3ea08 feat(ci): update spellchecker workflow (#143) 2024-11-23 17:30:05 +01:00
Marco Pasqualetti f2f041fe55 ci: test spellcheck via reviewdog (#137)
* ci: use GITHUB_TOKEN in spell checking workflow

* ci(spell-checking): use different reporter based on github context
2024-11-23 10:56:45 +01:00
renovate[bot] 9ec9700ed4 fix(deps): update dependency @types/node to v22.9.3 (#140) 2024-11-23 10:27:51 +01:00
renovate[bot] c6b047fc02 fix(deps): update dependency @types/node to v22.9.2 (#139) 2024-11-23 09:14:22 +01:00
Myan V. 6727264e39 Better error handling when trying to overwrite an existing folder and using a non-existent template (#135)
* fix: prevent overwriting existing project directories

* refactor: improve scaffold error messages
2024-11-22 13:38:54 +01:00
Valerio Ageno d1a5fe234d fix: examples folder 2024-11-21 19:16:06 +01:00
renovate[bot] e84c7cd276 fix(deps): update dependency turbo to v2.3.1 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 19:02:52 +01:00
Valerio Ageno 087813fab9 Add support for api/ folder (#129)
* feat: detect api/ folder from tuono CLI

* feat: prevent adding the API in the data endpoints list

* feat: create basic API handler proc macro

* feat: parse api/ and build .tuono/main.rs file

* chore: remove unused cargo dependencies

* refactor: tuono base health_check

* fix: remove failing test

* feat: update version to v0.14.0
2024-11-21 19:02:11 +01:00
Valerio Ageno 3a4e7398c6 feat: update documentation og:cover 2024-11-21 18:53:32 +01:00
Valerio Ageno 1630fd7e71 Update README.md 2024-11-21 11:30:02 +01:00
Myan V. 4aa99beaff Better error handling for target ports conflict (#126)
* feat: add error message when port 3000 is already in use

* feat: expand port availability checking

* refactor: improve port extraction code modularity

* fix: formatting

* fix: clippy

* fix: move port checking logic to tuono crate

* fix(tuono): make main async

* fix: tokio panicking when testing CLI

* feat: working port conflict detection in crate tuono

* fix: passing format and lint checks

* refactor: refactor cli by making it non-async

* refactor: move port constants to module scope and remove unnecessary Result return type from port checker
2024-11-21 09:45:08 +01:00
Valerio Ageno eaa0ba28d8 Update README.md 2024-11-20 18:57:43 +01:00
renovate[bot] 3f3e1120b2 chore(deps): update pnpm to v9.14.2 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:52:19 +01:00
Marco Pasqualetti 89d7a2c740 chore: add examples folder to workspaces and make examples packages name unique (#125) 2024-11-20 13:46:31 +01:00
Valerio Ageno 56c31f0204 feat: update version to v0.13.3 2024-11-20 13:45:37 +01:00
renovate[bot] 338cfb5be2 fix(deps): update dependency @types/node to v22.9.1 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:32 +01:00
renovate[bot] 43f6fe664b chore(deps): update pnpm to v9.14.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:03 +01:00
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
Valerio Ageno ea82d2b1df Correct documentation spelling (#68)
* fix: documentation/index spelling

* fix: documentation/development-setup spelling

* fix: documentation/api-fetching spelling

* fix: documentation/components spelling

* fix: documentation/dynamic-routes spelling

* feat: add correct tutorial link to README.md

* chore: remove legacy  folder

* fix: formatting

* fix: documentation/error-handling spelling

* fix: documentation/seo spelling

* fix: documentation/redirections spelling

* fix: documentation/production spelling

* fix: documentation/conclusion spelling

* feat: update version to v0.10.4

* fix: redirections spelling

* fix: API fetching spelling

* feat: create CLI doc page

* fix: correct CLI spelling

* fix: update CLI output spelling
2024-10-17 19:00:23 +02:00
renovate[bot] 93b44088a9 chore(deps): update dependency node to v20.18.0 (#60)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 18:09:15 +02:00
renovate[bot] 625c091fdc chore(deps): update dependency @testing-library/react to v16 (#63)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 18:08:57 +02:00
Valerio Ageno e0c0447fb6 fix: remove useless container 2024-10-17 12:17:38 +02:00
Valerio Ageno b67a8e8537 fix: remove table of contents 2024-10-17 12:14:33 +02:00
renovate[bot] c9f1b02954 chore(deps): update dependency jsdom to v25 (#64)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 09:47:47 +02:00
renovate[bot] 6b3bfca1f6 chore(deps): update dependency vitest to v2 (#65)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 09:47:29 +02:00
Valerio Ageno 15e568594d feat: update code font size 2024-10-17 09:03:05 +02:00
Valerio Ageno 089f41ee89 feat: update doc code example font size 2024-10-17 08:56:17 +02:00
Valerio Ageno 636c68e532 chore: update doc sidebar 2024-10-17 08:55:23 +02:00
Valerio Ageno 6d8a04dfb2 Move tutorial into web documentation (#54)
* chore: move tutorial into web documentation

* fix: update ci-documentation tuono version

* fix: rollback documentation website version to v0.10.1

* temporaly remove 'dynamic' keyword

* feat: update tuono documentation to v0.10.3

* chore: format

---------

Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-10-17 08:20:01 +02:00
Valerio Ageno 01d5aad8aa fix: update testing-library/jest-dom version 2024-10-16 20:49:46 +02:00
Valerio Ageno 002733cd99 Prevent dynamic fn replacement when not imported by tuono (#57)
* fix: prevent dynamic fn replacement when not imported by tuono

* feat: update version to v0.10.3

* Revert "fix(deps): update dependency zustand to v5 (#55)"

This reverts commit d7a086dfb1.

* Revert "fix(deps): update dependency zustand to v4.5.5 (#37)"

This reverts commit b36dc0b897.

* Revert "chore(deps): update dependency @testing-library/react to v16 (#38)"

This reverts commit 1a1b62698c.

* Revert "chore(deps): update dependency @tanstack/config to ^0.13.0 (#32)"

This reverts commit 6c7cc62371.

* revert pnpm update

* Revert "chore(deps): update dependency vitest to v2 (#45)"

This reverts commit c5b5f6aefe.

* Revert "chore(deps): update dependency node to v20.18.0 (#33)"

This reverts commit 588326cc57.
2024-10-16 20:41:33 +02:00
renovate[bot] d7a086dfb1 fix(deps): update dependency zustand to v5 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 19:38:29 +02:00
renovate[bot] d60c7dcd9b fix(deps): update rust crate watchexec-supervisor to v3 (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 19:29:33 +02:00
renovate[bot] 58baf8aa82 fix(deps): update dependency tuono to v0.10.2 (#51)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:57:44 +02:00
Valerio Ageno 0a8504d9f2 feat: update version to v0.10.2 (#50) 2024-10-13 10:18:19 +02:00
Valerio Ageno 934a83c0e2 Fix spelling (#49)
* fix: README spelling

* fix: CONTRIBUTING spelling

* fix: CODE_OF_CONDUCT

* fix: documentation spelling
2024-10-12 19:03:05 +02:00
Valerio Ageno ea65ad042f fix: documentation eslint 2024-10-12 19:02:17 +02:00
renovate[bot] 3544cd2943 chore(deps): update typescript-eslint monorepo to v8 (major) (#47)
* chore(deps): update typescript-eslint monorepo to v8

* chore: update ban-type eslint rule

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 18:40:04 +02:00
renovate[bot] e593707ce7 fix(deps): update rust crate syn to v2 (#48)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 18:38:44 +02:00
renovate[bot] 6dd0fc7410 chore(deps): update actions/checkout action to v4 (#46) 2024-10-12 13:55:40 +02:00
renovate[bot] faf7bf29e2 fix(deps): update rust crate tungstenite to 0.24.0 (#44) 2024-10-12 13:41:03 +02:00
renovate[bot] c5b5f6aefe chore(deps): update dependency vitest to v2 (#45) 2024-10-12 13:40:51 +02:00
renovate[bot] 8e48eec01d chore(deps): update dependency eslint-plugin-react-hooks to v5 (#42) 2024-10-12 13:37:33 +02:00
renovate[bot] e3c5cc882f chore(deps): update dependency jsdom to v25 (#43) 2024-10-12 13:37:15 +02:00
renovate[bot] dfb1bebc6b fix(deps): update rust crate tokio-tungstenite to 0.24.0 (#39)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:03:59 +02:00
renovate[bot] a40367f813 fix(deps): update rust crate tower-http to 0.6.0 (#40)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:03:44 +02:00
renovate[bot] 1a1b62698c chore(deps): update dependency @testing-library/react to v16 (#38)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:00:23 +02:00
renovate[bot] b36dc0b897 fix(deps): update dependency zustand to v4.5.5 (#37)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:55:01 +02:00
renovate[bot] a9eacf21f4 chore(deps): update pnpm to v9.12.1 (#36)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:51:50 +02:00
renovate[bot] 23d84b25be fix(deps): update rust crate ssr_rs to 0.7.0 (#35)
* fix(deps): update rust crate ssr_rs to 0.7.0

* chore: update ssr error

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:45:23 +02:00
renovate[bot] 6c7cc62371 chore(deps): update dependency @tanstack/config to ^0.13.0 (#32)
* chore(deps): update dependency @tanstack/config to ^0.13.0

* fix: tanstack/config vite import

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:40:03 +02:00
renovate[bot] 588326cc57 chore(deps): update dependency node to v20.18.0 (#33)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:34:06 +02:00
renovate[bot] ce654bf7a2 chore: Configure Renovate (#31)
* Add renovate.json

* fix: ignore benches/

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:29:14 +02:00
Valerio Ageno a3bcb76105 CI: add spell checking pipeline (#30)
* ci: add spell checking pipeline

* fix: add github to the lookup folders

* fix: add patterns rule

* refactor: Github action definition

* test: introduce spell bug

* fix: update spell checker reporter

* fix: update action version

* feat: remove disabled_categories

* feat: remove github token

* feat: add fail level to pipeline

* fix: error as string

* feat: remove disabled rules

* feat: add reviewdog_token

* feat: change disabled_rules to string

* feat: add spell rules

* fix: spell categories

* Update bug_report.md
2024-10-12 12:05:45 +02:00
Valerio Ageno 4045ebc24e Update README.md 2024-08-20 14:34:37 +02:00
Valerio Ageno 2be3710462 Update README.md 2024-08-20 14:30:46 +02:00
Valerio Ageno fe3185c6dc Update documentation version to v0.10.1 2024-08-19 21:03:50 +02:00
Valerio Ageno f10b813e1e Update ci-documentation.yml 2024-08-19 18:44:32 +02:00
Valerio Ageno c67d4199a0 fix: github language checks 2024-08-19 18:27:45 +02:00
184 changed files with 10293 additions and 1496 deletions
+5 -2
View File
@@ -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",
+9
View File
@@ -1 +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
-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.8.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.0
- 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@v3
- uses: actions-rs/toolchain@v1
with:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
dry-run: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
@@ -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@v3
- uses: actions-rs/toolchain@v1
with:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
check-repo: true
+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
+66
View File
@@ -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: ''
+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 @@
v20.10.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"
+15 -15
View File
@@ -2,7 +2,7 @@
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
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,
@@ -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
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
- 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 which could reasonably be considered inappropriate in a
- Other conduct that could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -45,13 +45,13 @@ 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 to this Code of Conduct, and will communicate reasons for moderation
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.
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.
@@ -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
+9 -6
View File
@@ -2,20 +2,23 @@
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
# How to contribute to Tuono
# 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 to [open a new issue](https://github.com/Valerioageno/tuono/issues/new)
or to reach me using my email address [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on twitter (X) DMs @valerioageno.
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.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number, if applicable.
- The pull requests must pass all the CI pipelines
+3 -3
View File
@@ -7,8 +7,8 @@ members = [
]
exclude = [
"apps/documentation",
"examples/mdx",
"examples/tuono",
"examples/tutorial",
"examples/wit-mdx",
"examples/tuono-app",
"examples/tuono-tutorial",
"benches/tuono"
]
+38 -57
View File
@@ -1,5 +1,5 @@
[Documentation](https://tuono.dev) | [Tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) |
[✨Contributing](https://tuono.dev/documentation/contributing)
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
# Tuono
@@ -8,91 +8,72 @@
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
**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** is the react/rust fullstack framework.
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
> Check out the [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) to get started.
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 fullstack right? (no)**
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)**
Tuono is a fullstack React framework with the server side written in Rust.
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
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 an hard language then writing server side code is hard as well right? (no again)**
**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 with a separate file alongside the React route. The routing is handled
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.
## Features
**How is Tuono different from Next.js?**
- 🟦 Typescript
- 🌐 Routing
- 🔥 Hot Module Reload
- 🍭 CSS modules
- 🧬 Server Side Rendering
- 🧵 Multi thread backend
- ⌨️ MDX support
- ⚙️ Build optimizations
- Custom APIs*
- Image optimization*
- Server streamed content*
> *Development in progress
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 to take a look at the
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
To download it is required [cargo](https://doc.rust-lang.org/cargo/) which is the [rust](https://www.rust-lang.org/)
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 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 inside the project folder `tuono dev`
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 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.
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`.
## Requirements
- rust
- cargo
- node
- npm/pnpm/yarn*
> yarn [pnp](https://yarnpkg.com/features/pnp) is not supported yet
## Folder structure
```
├── package.json
├── public
├── src
│ ├── routes
│ └── styles
├── Cargo.toml
├── README.md
└── tsconfig.json
```
## Contributing
Any help or suggestion will be appreciated and encouraged.
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
## License
+5 -2
View File
@@ -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",
+1 -1
View File
@@ -8,7 +8,7 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.10.0"
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.0"
"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": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"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": "^4.6.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: 42 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,12 +16,12 @@ export default function Hero(): JSX.Element {
return (
<Container size={1100} my={100}>
<Center>
<Title order={1}>The react/rust fullstack framework</Title>
<Title order={1}>Superfast fullstack react framework</Title>
</Center>
<Center mt={50}>
<Text fz="18px">
The technologies we love seamessly working together to unleash the
<strong> highest web performance</strong> ever met on react
The technologies we love seamlessly working together to unleash the
<strong> highest web performance</strong> ever met on React
</Text>
</Center>
<Center mt={50}>
@@ -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,8 +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} />
return <Code {...props} style={{ fontSize: 14 }} />
}
@@ -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,5 +1,5 @@
.pre {
code {
font-size: 16px;
font-size: 14px;
}
}
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { CodeHighlight } from '@mantine/code-highlight'
import styles from './mdx-pre.module.css'
@@ -9,7 +10,7 @@ export default function MdxPre({ children }: PreProps): JSX.Element {
<CodeHighlight
className={styles.pre}
code={children.props.children || ''}
style={{ borderRadius: 8 }}
style={{ borderRadius: 8, fontWeight: 100 }}
language={children.props.className?.replace('language-', '')}
/>
)
@@ -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>
)
}
@@ -2,5 +2,5 @@
border-radius: 8px;
margin-top: 0.25rem;
line-height: 1.25rem;
font-weight: bold;
font-weight: 500;
}
@@ -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,31 +1,87 @@
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">
<h3>Tutorial</h3>
<SidebarLink
href="/documentation/tutorial/intro"
label="Intro"
href="/documentation"
label="Getting started"
onClick={close}
/>
<h3>Documentation</h3>
<SidebarLink href="/documentation" label="Overview" onClick={close} />
<SidebarLink
href="/documentation/installation"
label="Installation"
onClick={close}
/>
<SidebarLink
href="/documentation/getting-started"
label="Getting started"
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"
defaultOpened
onClick={close}
>
<SidebarLink
@@ -34,12 +90,19 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
onClick={close}
/>
</SidebarLink>
<SidebarLink
label="Contributing"
href="/documentation/contributing"
onClick={close}
leftSection="✨"
/>
onClick={close}
>
<SidebarLink
href="/documentation/contributing/local-development"
label="Local development"
onClick={close}
/>
</SidebarLink>
</AppShell.Navbar>
)
}
@@ -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,8 +1,8 @@
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'
import TableOfContents from '../../components/table-of-content'
interface RootRouteProps {
children: ReactNode
@@ -11,14 +11,9 @@ interface RootRouteProps {
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
return (
<AppShell.Main>
<Container size="lg" p={20} display={{ lg: 'flex' }}>
<Container id="mdx-root" component="article" size="md" p={0}>
<MdxProvider>{children}</MdxProvider>
<EditPage />
</Container>
<Container size="xs" visibleFrom="lg" p={0}>
<TableOfContents withTabs={false} />
</Container>
<Container id="mdx-root" component="article" size="md" p={20}>
<MdxProvider>{children}</MdxProvider>
<EditPage />
</Container>
</AppShell.Main>
)
@@ -0,0 +1,55 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Application state"
canonical="https://tuono.dev/documentation/application-state"
description="Learn how to add features to your Tuono application"
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
# Application state
The main reason Tuono is fast is that it loads just the features that are needed for the project.
To define them, you need to fill the `ApplicationState` struct in the `./src/main.rs` file, and
they will be automatically available in all the handlers you will define across the application.
```rs
// src/main.rs
#[derive(Clone)]
pub struct ApplicationState {
pub website_name: String,
pub base_path: String
}
pub fn main() -> ApplicationState {
let website_name = "tuono".to_string();
let base_path = "http://localhost:3000".to_string();
ApplicationState {
website_name,
base_path
}
}
```
> For the sake of simplicity here we just enable a `String` but you can add Database connections or
> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client).
Now the `ApplicationState` is available on all the handlers as following:
```rs
// src/routes/index.rs
#[tuono_lib::handler]
// The first argument always is the Request
// The ApplicationState arguments are optional. You can use just the ones you need
// to use in the handler (with no specific order).
async fn my_handler(req: Request, website_name: String) -> Response {
...
}
```
@@ -0,0 +1,44 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - CLI"
canonical="https://tuono.dev/documentation/cli"
description="Tuono is the CLI that provides all the needed commands to handle the full-stack project."
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
# CLI
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
> `tuono` CLI yet.
To list all the available commands, run `tuono -h`.
```bash
The React/Rust full-stack framework
Usage: tuono <COMMAND>
Commands:
dev Start the development environment
build Build the production assets
new Scaffold a new project
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
```
To create a new project, run `tuono new [NAME]` (optionally you can pass the --template (or -t) flag - check the examples' folder).
Then to run the local development environment run inside the project folder `tuono dev`
Finally, when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
Now to execute it, just run `cargo run --release`.
@@ -1,37 +1,37 @@
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
## TL;DR
The project is massive - if you like it do consider to contribute!
The project is massive - if you like it, do consider contributing!
## Getting started
The `tuono` project can mostly be split by the following sub-domains:
The `tuono` project can mostly be split into the following subdomains:
- The CLI
- The rust backend
- The react frontend
- The Rust backend
- The React frontend
- The documentation website (which is written with tuono 🚀)
To check what are the knowledge requirement for each domain check the
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 me using
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)
on Twitter (X) DMs `@valerioageno`, [Linkedin](https://www.linkedin.com/in/valerioageno)
and discord `@__v__v__`.
## Requirements
@@ -39,10 +39,10 @@ and discord `@__v__v__`.
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:
Without taking into account specific cases, we can mostly split the domain requirements by:
- The `CLI` needs Rust knowledge (even though a couple of scenario might need also Typescript)
- 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
- 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.
@@ -1,30 +0,0 @@
import { Head } from 'tuono'
<Head>
<title>Tuono - Getting started</title>
</Head>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Getting started" />
</Breadcrumbs>
# Getting started
## The CLI
Tuono is the CLI that provides all the needed commands to handle the fullstack 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`.
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`.
@@ -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 fullstack framework for building React applications using Rust as
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 fullstack right? (no)**
Tuono is a fullstack React framework with the server side written in Rust.
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
React is still React - it is just superpowered.
**Rust is an 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 with 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 thread 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,37 +1,38 @@
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
## Requirements
`Tuono` is a development environment built in rust and typescript that outputs a website written with both the
languages. Since that you need the following tools installed on your computer to work with it:
`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 toolchain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
- `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
- `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
> NodeJs is needed just for the development environment. The final output will run on a rust server.
> 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 on a terminal:
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 is correctly installed run:
To check that it is correctly installed, run:
```bash
tuono --version
@@ -40,7 +41,7 @@ tuono --version
Run `tuono -h` to see all the available commands.
```bash
The react/rust fullstack framework
The React/Rust full-stack framework
Usage: tuono <COMMAND>
@@ -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
@@ -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, lets fetch all of them from the root page. Since we want to render them on the server side, we are going to need to implement the logic in the `index.rs` file.
Clear the `index.rs` file and paste:
```rust
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use reqwest::Client;
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemons {
results: Vec<Pokemon>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
url: String,
}
#[tuono_lib::handler]
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap();
Response::Props(Props::new(data))
}
Err(_err) => Response::Props(Props::new("{}")),
};
}
```
> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers.
> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional.
The terminal will complain now for two reasons:
1. We don't have imported any `reqwest` crate
2. The second argument `fetch: Client` has not been defined yet as global state.
Let's fix these in the next section.
## Application state
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application.
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
To install it just run in your terminal:
```bash
$ cargo add reqwest
```
A new entry has just been added to your `Cargo.toml` file.
```diff
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.14.0" # the version might be different
serde = { version = "1.0.202", features = ["derive"] }
++ reqwest = "0.12.9" # the version might be different
```
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
> (similarly as the package.json for Javascript).
Now let's define the `ApplicationState` in the `./src/main.rs` file.
```rs
// Import here the just added reqwest library
use reqwest::Client;
#[derive(Clone)]
// Extend this struct with the feature you will need for your application
pub struct ApplicationState {
// This will be available to all your route handlers
pub fetch: Client,
}
pub fn main() -> ApplicationState {
let fetch = Client::new();
return ApplicationState { fetch };
}
```
Now the `fetch: Client` argument is available in the above defined handler, and the terminal should not complain anymore.
Let's see in the next section how to show the fetched data on the browser.
## Handling the page UI
Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste:
```tsx
// src/routes/index.tsx
import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element | null {
if (!data?.results) {
return null
}
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
{data.results.map((pokemon) => {
return pokemon.name
})}
</ul>
</>
)
}
```
Refresh the browser now! A bit ugly, but all the Pokémon are finally printed on screen!
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
prev={{
title: 'Development setup',
href: '/documentation/tutorial/development-setup',
}}
next={{ title: 'Components', href: '/documentation/tutorial/components' }}
/>
@@ -0,0 +1,145 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Components"
canonical="https://tuono.dev/documentation/tutorial/components"
description="Learn how to manage the components in a Tuono codebase"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Components' },
]}
/>
# Components
## Creating a stand-alone component
Lets 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, lets 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 havent yet implemented the `pokemons/[pokemon]` page.
As previously said, CSS modules are enabled out of the box, so lets 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
Thats 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 Rusts 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. Lets 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`.
Lets 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 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'
export default function Pokemon({ data }: TuonoProps): JSX.Element {
return <PokemonView pokemon={data} />
}
```
The browser should complain that the component `PokemonView` does not exist. Lets 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',
}}
/>
@@ -1,17 +0,0 @@
import { Head } from 'tuono'
<Head>
<title>Tuono - Tutorial intro</title>
</Head>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" />
</Breadcrumbs>
# Tutorial
You can find the tutorial [here](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
Other tasks have the priority right now. The tutorial will be ported here as soon as possible.
@@ -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',
}}
/>
+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 fullstack framework</title>
<meta
name="description"
content="The technologies we love seamessly 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());
+7 -5
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono"
version = "0.10.1"
version = "0.14.1"
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,14 +21,15 @@ 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 = "2.0.0"
watchexec-supervisor = "3.0.0"
glob = "0.3.1"
regex = "1.10.4"
reqwest = {version = "0.12.4", features =["blocking", "json"]}
serde_json = "1.0"
fs_extra = "1.3.0"
http = "1.1.0"
+41 -1
View File
@@ -1,6 +1,11 @@
use glob::glob;
use glob::GlobError;
use http::Method;
use std::collections::hash_set::HashSet;
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 +20,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 +37,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();
@@ -102,6 +117,24 @@ impl App {
.spawn()
.expect("Failed to run the rust server")
}
pub fn get_used_http_methods(&self) -> HashSet<Method> {
let mut acc = HashSet::new();
for (_, route) in self.route_map.clone().into_iter() {
if route.axum_info.is_some() {
acc.insert(Method::GET);
}
if !route.is_api() {
continue;
}
for method in route.api_data.unwrap().methods.into_iter() {
acc.insert(method);
}
}
acc
}
}
#[cfg(test)]
@@ -119,6 +152,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 +167,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"),
];
+35 -1
View File
@@ -11,6 +11,9 @@ use crate::scaffold_project;
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
use crate::watch;
const TUONO_PORT: u16 = 3000;
const VITE_PORT: u16 = 3001;
#[derive(Subcommand, Debug)]
enum Actions {
/// Start the development environment
@@ -33,7 +36,7 @@ enum Actions {
}
#[derive(Parser, Debug)]
#[command(version, about = "The react/rust fullstack framework")]
#[command(version, about = "The React/Rust full-stack framework")]
struct Args {
#[command(subcommand)]
action: Actions,
@@ -47,12 +50,41 @@ fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
Ok(())
}
fn check_ports(mode: Mode) {
let rust_listener = std::net::TcpListener::bind(format!("0.0.0.0:{TUONO_PORT}"));
if let Err(_e) = rust_listener {
eprintln!("Error: Failed to bind to port {}", TUONO_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
TUONO_PORT
);
std::process::exit(1);
}
if mode == Mode::Dev {
let vite_listener = std::net::TcpListener::bind(format!("0.0.0.0:{VITE_PORT}"));
if let Err(_e) = vite_listener {
eprintln!("Error: Failed to bind to port {}", VITE_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
VITE_PORT
);
std::process::exit(1);
}
}
}
pub fn app() -> std::io::Result<()> {
let args = Args::parse();
match args.action {
Actions::Dev => {
check_ports(Mode::Dev);
init_tuono_folder(Mode::Dev)?;
watch::watch().unwrap();
}
Actions::Build { ssg } => {
@@ -68,6 +100,8 @@ pub fn app() -> std::io::Result<()> {
app.build_react_prod();
if ssg {
check_ports(Mode::Prod);
println!("SSG: generation started");
let static_dir = PathBuf::from("out/static");
+56 -2
View File
@@ -1,17 +1,19 @@
use fs_extra::dir::create_all;
use http::Method;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AxumInfo {
// Path for importing the module
pub module_import: String,
@@ -32,6 +34,7 @@ impl AxumInfo {
.to_string()
.replace('/', "_")
.replace('.', "_dot_")
.replace('-', "_hyphen_")
.to_lowercase();
if axum_route.is_empty() {
@@ -47,6 +50,7 @@ impl AxumInfo {
.as_str()
.to_string()
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace(']', ""),
axum_route: axum_route.replace('[', ":").replace(']', ""),
@@ -63,11 +67,56 @@ impl AxumInfo {
// TODO: to be extended with common scenarios
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
#[derive(Debug, PartialEq, Eq)]
// TODO: Refine this function to catch
// if the methods are commented.
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
regex
.find_iter(&file)
.map(|proc_macro| {
let http_method = proc_macro
.as_str()
// Extract just the element surrounded by the phrantesist.
.replace("tuono_lib::api(", "")
.replace(")]", "");
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
})
.collect::<Vec<Method>>()
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ApiData {
pub methods: Vec<Method>,
}
impl ApiData {
pub fn new(path: &String) -> Option<Self> {
if !path.starts_with("/api/") {
return None;
}
let base_path = std::env::current_dir().expect("Failed to get the base_path");
let file_path = base_path
.join(format!("src/routes{path}.rs"))
.to_str()
.unwrap()
.to_string();
let methods = read_http_methods_from_file(&file_path);
Some(ApiData { methods })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
path: String,
pub is_dynamic: bool,
pub axum_info: Option<AxumInfo>,
pub api_data: Option<ApiData>,
}
impl Route {
@@ -76,9 +125,14 @@ impl Route {
path: cleaned_path.clone(),
axum_info: None,
is_dynamic: has_dynamic_path(&cleaned_path),
api_data: ApiData::new(&cleaned_path),
}
}
pub fn is_api(&self) -> bool {
self.api_data.is_some()
}
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self.path.clone()))
}
+9 -3
View File
@@ -42,7 +42,7 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
let folder = folder_name.unwrap_or(".".to_string());
// In case of missing select the tuono example
let template = template.unwrap_or("tuono".to_string());
let template = template.unwrap_or("tuono-app".to_string());
let client = blocking::Client::builder()
.user_agent("")
@@ -63,11 +63,17 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
.collect::<Vec<&GithubFile>>();
if new_project_files.is_empty() {
println!("Template not found: {template}");
return;
eprintln!("Error: Template '{template}' not found");
println!("Hint: you can view the available templates at https://github.com/Valerioageno/tuono/tree/main/examples");
std::process::exit(1);
}
if folder != "." {
if Path::new(&folder).exists() {
eprintln!("Error: Directory '{folder}' already exists");
println!("Hint: you can scaffold a tuono project within an existing folder with 'cd {folder} && tuono new .'");
std::process::exit(1);
}
create_dir(&folder).unwrap();
}
+59 -22
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
}
@@ -77,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
module_import,
} = axum_info.as_ref().unwrap();
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
));
}
}
}
}
@@ -131,22 +146,44 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
&create_modules_declaration(&app.route_map),
)
.replace("/*VERSION*/", crate_version!())
.replace("/*MODE*/", mode.as_str());
let has_server_handlers = app
.route_map
.iter()
.filter(|(_, route)| route.axum_info.is_some())
.collect::<Vec<(&String, &Route)>>()
.is_empty();
if !has_server_handlers {
return src.replace(
"// AXUM_GET_ROUTE_HANDLER",
"use tuono_lib::axum::routing::get;",
.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 mut import_http_handler = String::new();
let used_http_methods = app.get_used_http_methods();
for method in used_http_methods.into_iter() {
let method = method.to_string().to_lowercase();
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
}
src
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
}
pub fn check_tuono_folder() -> io::Result<()> {
+1 -1
View File
@@ -77,7 +77,7 @@ pub async fn watch() -> Result<()> {
if should_reload_rust_server {
println!(" Reloading...");
run_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
run_server.start();
}
+11 -10
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono_lib"
version = "0.10.1"
version = "0.14.1"
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,12 +15,9 @@ include = [
"Cargo.toml"
]
[lib]
name = "tuono_lib"
path = "src/lib.rs"
[dependencies]
ssr_rs = "0.5.5"
ssr_rs = "0.7.0"
axum = {version = "0.7.5", features = ["json", "ws"]}
tokio = { version = "1.37.0", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
@@ -30,12 +28,15 @@ reqwest = {version = "0.12.4", features = ["json", "stream"]}
once_cell = "1.19.0"
regex = "1.10.5"
either = "1.13.0"
tower-http = {version = "0.5.2", features = ["fs"]}
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.10.1"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.14.1"}
# Match the same version used by axum
tokio-tungstenite = "0.21.0"
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tungstenite = "0.23.0"
tungstenite = "0.24.0"
http = "1.1.0"
pin-project = "1.1.7"
tower = "0.5.1"
+2 -2
View File
@@ -1,4 +1,5 @@
mod catch_all;
mod logger;
mod manifest;
mod mode;
mod payload;
@@ -14,9 +15,8 @@ pub use payload::Payload;
pub use request::Request;
pub use response::{Props, Response};
pub use server::Server;
pub use tuono_lib_macros::handler;
pub use tuono_lib_macros::{api, 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
+3 -3
View File
@@ -1,5 +1,5 @@
use crate::mode::{Mode, GLOBAL_MODE};
use ssr_rs::Ssr;
use ssr_rs::{Ssr, SsrError};
use std::cell::RefCell;
use std::fs::read_to_string;
use std::path::PathBuf;
@@ -10,7 +10,7 @@ use std::path::PathBuf;
pub struct Js;
impl Js {
pub fn render_to_string(payload: Option<&str>) -> Result<String, &'static str> {
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
if *mode == Mode::Dev {
@@ -36,7 +36,7 @@ impl ProdJs {
struct DevJs;
impl DevJs {
pub fn render_to_string(params: Option<&str>) -> Result<String, &'static str> {
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
Ssr::from(
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
.expect("Server bundle not found"),
+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());
+4 -3
View File
@@ -1,8 +1,9 @@
[package]
name = "tuono_lib_macros"
version = "0.10.1"
version = "0.14.1"
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"
@@ -18,5 +19,5 @@ crate_type = ["proc-macro"]
proc-marco = true
[dependencies]
syn = { version = "1.0.59", features = ["full"] }
syn = { version = "2.0.0", features = ["full"] }
quote = "1.0"
+69
View File
@@ -0,0 +1,69 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat};
pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let http_method = parse_macro_input!(attrs as Ident)
.to_string()
.to_lowercase();
let api_fn_name = Ident::new(
&format!("{}__tuono_internal_api", http_method),
Span::call_site().into(),
);
let fn_name = &item.sig.ident;
let return_type = &item.sig.output;
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! {
#application_state_import
#item
pub async fn #api_fn_name(#axum_arguments)#return_type {
#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(), #argument_names).await
}
}
.into()
}
+48 -15
View File
@@ -1,43 +1,76 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
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()
+7
View File
@@ -1,9 +1,16 @@
extern crate proc_macro;
use proc_macro::TokenStream;
mod api;
mod handler;
mod utils;
#[proc_macro_attribute]
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
handler::handler_core(args, item)
}
#[proc_macro_attribute]
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
api::api_core(args, item)
}
+47
View File
@@ -0,0 +1,47 @@
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_quote, FnArg, Pat, Stmt};
pub fn create_struct_fn_arg() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
})
.unwrap()
}
pub 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
}
pub 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
}
pub fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
pub fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
-12
View File
@@ -1,12 +0,0 @@
# Documentation
This folder contains the documentation related to the `tuono` framework.
The framework expects to enable SSG and MDX support in the next months.
At that time a standalone website will be built with tuono to hold the whole documentation.
The following docs are currently actively maintained:
- [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
## Contributions
Any help or suggestion will be strongly appreciated.
-697
View File
@@ -1,697 +0,0 @@
# Tuono tutorial
This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve along the development - be sure to have installed the latest version.
The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction).
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or on Twitter (X) DMs
[@valerioageno](https://twitter.com/valerioageno)
This tutorial is **not meant** for people that don't know React - in that case I suggest you to first read the [React doc](https://react.dev/);
Typescript and Rust knowledge is not a requirement though!
## Table of Content
* [CLI Installation](#cli-installation)
* [Project scaffold](#project-scaffold)
* [Start the dev environment](#start-the-dev-environment)
* [The “/” route](#the--route)
* [Tutorial introduction](#tutorial-introduction)
* [Fetch all the pokemons](#fetch-all-the-pokemons)
* [Create a stand-alone component](#create-a-stand-alone-component)
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
* [Error handling](#error-handling)
* [Seo and meta tags](#seo-and-meta-tags)
* [Handle redirections](#handle-redirections)
* [Building for production](#building-for-production)
* [Conclusion](#conclusion)
## CLI Installation
The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
```bash
$ cargo install tuono
```
To check that is correctly installed run:
```bash
$ tuono --version
```
Run `tuono -h` to see all the available commands.
## Project scaffold
To setup a new fresh project you just need to run the following command:
```bash
$ tuono new tuono-tutorial
```
Get into the project folder and install the dependencies with:
```bash
$ npm install
```
Open it with your favourite 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 (⚠️ CSS modules on routes are forbidden).
## Start the dev environment
To start the development environment you just need to run the following command within the project folder:
```bash
$ tuono dev
```
The first time might take a little bit because it will install all the rusts 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.
## The “/” route
All the `index.tsx` files represent the folder root page (i.e. `src/routes/posts/index.tsx` is [`http://localhost:3000/posts`](http://localhost:3000/posts) as well as `src/routes/posts.tsx`).
The file `index.rs` represents the server side capabilities for the index route. On this file you can:
- Passing server side props
- Changing http status code
- Redirecting to a different route
## Tutorial introduction
Now that we have some knowledge about the project structure lets start the real tutorial.
The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a dynamic page for each one separately.
> If you have already installed the tuono CLI and you prefer read the code instead of writing it yourself
you can download the tutorial source with `tuono new tuono-tutorial --template tutorial`
## Fetch all the pokemons
To start lets fetch all of them in the root page; since we want to render them on the server side we gonna 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 tuono_lib::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("{}")),
};
}
```
Now the pokemons 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 { TuonoProps } from "tuono";
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>;
}
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 now the browser! A bit ugly but all the pokemons are finally printed on screen!
## Create a stand-alone component
Lets then create the button needed for displaying the list of pokemons.
Create the following file `src/components/PokemonLink.tsx` and fill the content with:
```tsx
// src/components/PokemonLink.tsx
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 lets import it in 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 havent yet implemented the `pokemons/[pokemon]` page.
As previously said CSS modules are enabled out of the box so lets 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 { 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>
);
}
```
## Create the `/pokemons/[pokemon]` route
The homepage is ready. We have the full list of pokemons and they are all links. Great!
Now we want to make those links actually pointing 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`.
These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`.
Lets 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 tuono_lib::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 lets work on the frontend. Paste into the `[pokemon].tsx` file the following code:
```tsx
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. Lets 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>
);
}
```
```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;
}
```
## 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 be initialized with also the
`Props::new_with_status()` method.
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 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,
)),
};
}
```
```diff
// src/routes/index.rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::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 pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
correctly receive a 404 status code in the console.
## 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` exposes also 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 { 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>
</>
);
}
```
```diff
// src/routes/pokemons/[pokemon].tsx
-- 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.
## Handle redirections
What if there is a pokemon 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
pokemon of the first generation.
First let's create a new route by just creating an new file `/pokemons/GOAT.rs` and pasting the following code:
```rs
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> 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!
## Building for production
The source now is ready to be released. Both server and client have been managed in a unoptimized way
to easy 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 then the prodiction 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 seamessly renders a React application!🚀
> Note: The `out` directory is not standalone. You can't rely just on it to run the production server.
## Conclusion
Thats 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).
-11
View File
@@ -15,14 +15,3 @@ $ tuono new [NAME] --template [TEMPLATE]
```
`[TEMPLATE]` is the folder name.
## Troubleshooting for contributors
There is a common error that happens during example development due to
pnpm workspace about the findings of different react versions.
To overcome this issue run within the single example folder:
```
pnpm install --ignore-workspace && pnpm upgrade --save
```
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "tuono-app"
version = "0.0.1"
edition = "2021"
@@ -1,6 +1,6 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"name": "tuono-app",
"description": "Basic tuono application",
"version": "0.0.1",
"dependencies": {
"react": "18.3.1",

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