Compare commits

...

156 Commits

Author SHA1 Message Date
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
Valerio Ageno d0a6c59c62 Add response time to SSR GET log (#29)
* feat: add response time to SSR GET log

* feat: update version to v0.10.1
2024-08-19 18:25:00 +02:00
Valerio Ageno 60f622542f feat: update tuono version in deploy documentation CD 2024-08-18 12:36:51 +02:00
Valerio Ageno e7fcd64b3a feat: add sitemaps to documentation website 2024-08-18 12:25:12 +02:00
Valerio Ageno 3772e7307e feat: add favicon to documentation website 2024-08-18 11:32:57 +02:00
Valerio Ageno e853db164d fix: documentation types CI 2024-08-18 11:25:14 +02:00
Valerio Ageno 509283adab feat: update tuono version to v0.10.0 2024-08-18 11:19:54 +02:00
Valerio Ageno db56b7f2dc feat: remove default head meta tags (#28) 2024-08-18 11:17:38 +02:00
Valerio Ageno acd663d79f Add custom response type (#27)
* feat: support route names with dots

* feat: add support for Response::Custom value

* feat: statically export xml files
2024-08-18 11:09:27 +02:00
Valerio Ageno 3636268a71 fix: documentation hero 2024-08-17 20:16:26 +02:00
Valerio Ageno 6402f42d01 fix: documentation mdx container 2024-08-17 20:08:23 +02:00
Valerio Ageno 71ac19cbb9 fix: documentation padding 2024-08-17 17:35:45 +02:00
Valerio Ageno d99389a398 fix: prod TOC 2024-08-17 17:32:25 +02:00
Valerio Ageno 024c386af4 feat: add table of content 2024-08-17 17:21:29 +02:00
Valerio Ageno d982c9c24b feat: change documenation border radious 2024-08-17 16:23:57 +02:00
Valerio Ageno 480973318d feat: add X button on documentation website 2024-08-17 15:37:21 +02:00
Valerio Ageno a497937106 feat: update benches to speed up nextjs servers 2024-08-17 15:00:43 +02:00
Valerio Ageno 0d33150f30 feat: add benchmarks against nextjs (#26) 2024-08-16 16:23:39 +02:00
Valerio Ageno 94667b1454 Update documentation tuono version to v0.9.2 2024-08-16 11:38:24 +02:00
Valerio Ageno 2cbcd8ad70 feat: update version to v0.9.2 2024-08-16 11:28:42 +02:00
Valerio Ageno 9f40a55907 feat: add id to documentation headings 2024-08-16 11:27:29 +02:00
Valerio Ageno 51e01ec142 feat: add link support to hash 2024-08-16 11:21:18 +02:00
Valerio Ageno 63fc2b9855 fix: documentation mailto link 2024-08-16 11:05:49 +02:00
Valerio Ageno edd8a02423 fix: documentation type checking 2024-08-15 21:16:38 +02:00
Valerio Ageno a0a81b514c fix: documentation formatting 2024-08-15 21:08:48 +02:00
Valerio Ageno 92820804f8 fix: sidebar on mobile 2024-08-15 21:08:18 +02:00
Valerio Ageno e7ea7ed1c4 feat: improve contributing documentation 2024-08-15 21:07:28 +02:00
Valerio Ageno 7471c90f49 feat: update documentation tuono version to v0.9.1 2024-08-15 20:52:22 +02:00
Valerio Ageno f545a9c469 feat: update contributing and tutorial pages 2024-08-15 20:51:58 +02:00
Valerio Ageno 30073c226d feat: update version to v0.9.1 2024-08-15 20:44:25 +02:00
Valerio Ageno 1b1e91b6c0 fix: sanitize client side pathnames (#25) 2024-08-15 20:42:47 +02:00
Valerio Ageno 71d7ccf27d feat: update README header 2024-08-15 20:36:11 +02:00
Valerio Ageno 3ca24ec87f feat: create overview and getting started pages 2024-08-15 19:25:03 +02:00
Valerio Ageno b2940d9fa8 feat: add breadcrumbs to documentation 2024-08-15 18:52:56 +02:00
Valerio Ageno a7b7f0eac2 feat: update documentation sidebar 2024-08-15 18:20:20 +02:00
Valerio Ageno 4859916edd feat: update documentation sidebar 2024-08-15 11:31:33 +02:00
Valerio Ageno ad3e19e965 fix: documentation sidebar on mobile 2024-08-14 21:28:23 +02:00
Valerio Ageno 03dead1083 feat: update documentation tuono version to v0.9.0 2024-08-14 20:59:43 +02:00
Valerio Ageno 341fba6ca9 Preload chunks when links are visible in the viewport (#24)
* feat: preload chunk when links are visible in the viewport

* feat: update version to v0.9.0
2024-08-14 20:50:47 +02:00
Valerio Ageno 58c4890122 Update pull_request_template.md 2024-08-14 20:45:37 +02:00
Valerio Ageno af42e5ef21 fix: remove useless pendingElement 2024-08-14 14:48:26 +02:00
Valerio Ageno 5abff0384c feat: update documentation navbar 2024-08-14 14:45:47 +02:00
Valerio Ageno cc915560b8 update documentation tuono version to v0.8.4 2024-08-14 14:41:22 +02:00
Valerio Ageno 9835a4c0d1 fix: route switch wrappers re-rendering (#23)
* fix: route switch wrappers re-rendering

* feat: update version to v0.8.4
2024-08-14 14:26:52 +02:00
Valerio Ageno 90782ae710 fix: turbo dev task config 2024-08-11 18:19:38 +02:00
Valerio Ageno 75c790dd6e refactor: create standalone router package (#22)
* refactor: create standalone router package

* fix: add jsdom to tuono-router package

* fix: remove test in tuono vite.config

* update turbo to latest

* fix: wait build before lint

* fix: router types
2024-08-11 17:14:54 +02:00
Valerio Ageno 8604858a61 fix: documentation formatting 2024-08-11 16:33:45 +02:00
Valerio Ageno d1b6162d3a Update contributing.mdx 2024-08-09 17:48:23 +02:00
Valerio Ageno a445644b47 Update contributing.mdx 2024-08-09 17:47:07 +02:00
Valerio Ageno ebb4434506 Update contributing.mdx 2024-08-09 17:40:42 +02:00
Valerio Ageno 36a53662a0 Update contributing.mdx 2024-08-09 17:40:18 +02:00
Valerio Ageno 569d95dc9c fix: routing page 2024-07-27 14:58:18 +02:00
Valerio Ageno bc4c4e03f0 feat: update documentation tuono version (#21) 2024-07-27 14:52:02 +02:00
Valerio Ageno 170c5ea098 Update static build html structures (#20)
* fix: improve static build output structure

* feat: update version to v0.8.3
2024-07-27 14:42:28 +02:00
Valerio Ageno 3fd5f166e2 fix: edit page github link 2024-07-27 11:40:24 +02:00
Valerio Ageno a2285a78a1 fix: documentation output folder 2024-07-27 11:38:18 +02:00
Valerio Ageno 1ae7f72de3 Create documentation website (#19)
* feat: scaffold documentation app

* feat: init documentation website with mantine

* feat: add theme button

* feat: setup mdx link component

* feat: setup basic documentation folder

* feat: add routing folder

* feat: scaffold homepage

* feat: refined navbar and hero

* feat: create pre mdx component

* feat: create mdx blockquote component

* feat: create code mdx component

* fix: remove HTMLProps type

* feat: create base hero page

* ci: add lint/formatting/type checking to documentation

* ci: add documentation pipeline

* fix: static build documentation

* fix: remove documentation test check

* fix: install deps outside workspace

* fix: filter prettier checks

* fix: eslint plugins

* fix: clone eslintrc

* feat: update dark theme colors

* doc: add installation page

* feat: add deploy documenation CD pipeline

* fix: update documentation bucket region

* feat: update documentation head tags

* fix: deploy website on main branch push
2024-07-27 11:33:34 +02:00
Valerio Ageno 19782a2dbe feat: update version to v0.8.2 2024-07-25 19:06:15 +02:00
Valerio Ageno b755e658f5 fix: add mdx file to watch script 2024-07-25 19:06:15 +02:00
Valerio Ageno cc0279b964 fix: typescript check 2024-07-25 13:45:29 +02:00
Valerio Ageno 099da549b5 fix: eslint check on vite config 2024-07-25 13:43:24 +02:00
Valerio Ageno 92a76032eb feat: update version to v0.8.1 2024-07-24 20:59:49 +02:00
Valerio Ageno cd17eebf96 fix: mdx vite configuration 2024-07-24 20:58:09 +02:00
Valerio Ageno 1726312d34 Support nested roots (#18)
* feat: remove isLoader route property

* fix: has-parent-route fs-router util

* feat: handle routeGenerator nested __root

* feat: enable multiple roots with nested dynamic paths

* feat: enable nested routes

* fix: prevent route path creation for __root

* feat: prevent wrong path on subfolder index routes

* chore: cleaned tutorial example folder

* feat: update version to v0.8.0
2024-07-23 19:13:18 +02:00
Valerio Ageno 444d1479b0 Add support for MDX (#17)
* feat: add support for MDX

* fix: prevent empty server handler error message

* doc: add MDX mention to README

* feat: update version to v0.7.0
2024-07-22 07:59:43 +02:00
Valerio Ageno 2949ea4dfa Support Static Site Generation (SSG) (#16)
* refactor: init routes within App::new

* feat: handle build and server run within App struct

* feat: prevent dynamic routes SSG

* feat: support SSG

* chore: remove debugger

* feat: update version to v0.6.0
2024-07-21 14:10:12 +02:00
Valerio Ageno 8ab292b2f9 Update pull_request_template.md 2024-07-15 13:40:49 +02:00
Valerio Ageno 9d8bfd9826 Refactor application struct representation (#15)
* feat: add ssg module and CLI argument

* refactor: tuono lib.rs structure

* refactor: create App struct

* feat: extend file extensions to read in src/routes/

* feat: load all routes in the app.route_map

* refactor: creaded AxumInfo struct
2024-07-14 20:58:03 +02:00
Valerio Ageno 8a6685a6d0 Update README.md 2024-07-13 12:10:13 +02:00
Valerio Ageno 78990d3c08 Add support for meta tags (#14)
* feat: added support for head meta tags

* doc: update tutorial

* feat: add support to no css manifest entries

* feat: update version to v0.5.0

* doc: add mention to add the imports field

* feat: add filter to workspace package.json

* fix: add types declarations for react-meta-tags
2024-07-13 12:09:56 +02:00
Valerio Ageno 227662489b Update README.md 2024-07-12 11:01:55 +02:00
Valerio Ageno 5ce410f233 Update bug_report.md 2024-07-12 10:55:47 +02:00
Valerio Ageno 67d4e1bcac Create use router hook (#13)
* refactor: useRouter to useInternalRouter

* feat: create useRouter hook

* feat: optimized payload sent to client

* feat: add support for query and pathname value in useRouter

* feat: update version to v0.4.6
2024-07-11 21:43:38 +02:00
Valerio Ageno 3e6aa540fa Update README.md 2024-07-11 15:54:53 +02:00
Valerio Ageno b412371287 Remove lifetime notation on Request (#12)
* feat: remove lifetime notation on Request

* feat: update version to v0.4.5
2024-07-09 19:00:48 +02:00
Valerio Ageno f62fc0b2b9 Create pull_request_template.md 2024-07-09 12:35:09 +02:00
Valerio Ageno 35d8154649 Create README.md into doc folder 2024-07-09 09:07:04 +02:00
Valerio Ageno 6e794e9500 Update issue templates 2024-07-09 08:38:27 +02:00
256 changed files with 10652 additions and 1559 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
+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
@@ -0,0 +1,36 @@
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
+13
View File
@@ -0,0 +1,13 @@
## Context & Description
<!--
Thank you for your Pull Request.
Explain the context and why you're making that change. What is the problem
you're trying to solve? If a new feature is being added, describe the intended
use case that feature fulfills.
Bug fixes and new features should include tests.
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
-->
+70
View File
@@ -0,0 +1,70 @@
name: Documentation Website CI
on:
push:
paths:
- '.github/**'
- 'apps/documentation/**'
pull_request:
paths:
- '.github/**'
- 'apps/documentation/**'
jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
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
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.4
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build project
working-directory: ./apps/documentation
run: tuono build --static
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
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
- name: Check formatting
run: pnpm docs:format:check
- name: Lint
run: pnpm docs:lint
- name: Types
run: pnpm docs:types
@@ -0,0 +1,55 @@
name: Deploy documentation website on AWS S3
on:
push:
branches:
- main
paths:
- '.github/**'
- 'apps/documentation/**'
jobs:
deploy-documentation:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.4
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build project
working-directory: ./apps/documentation
run: tuono build --static
- name: Deploy
uses: reggionick/s3-deploy@v4
with:
folder: apps/documentation/out/static
bucket: tuono-documentation
bucket-region: eu-west-3
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
invalidation: /
delete-removed: true
no-cache: true
private: true
files-to-include: '{.*/**,**}'
+13 -13
View File
@@ -23,26 +23,26 @@ jobs:
run_install: false
- name: Install dependencies
run: pnpm install
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:
- 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 }}
@@ -71,7 +71,7 @@ jobs:
run: cp README.md packages/tuono/README.md
- name: Install dependencies
run: pnpm install
run: pnpm install
- name: Build
run: pnpm build
@@ -85,13 +85,13 @@ 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:
- 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
+21
View File
@@ -0,0 +1,21 @@
name: Spell Checker
on: [pull_request]
jobs:
spell_checking:
name: runner / LanguageTools
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
reporter: 'github-pr-review'
# Change reporter level if you need.
level: 'error'
patterns: '**/*.md **/*.txt **/*.mdx'
#enabled_categories: 'STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END'
disabled_categories: ''
disabled_rules: ''
+39 -39
View File
@@ -15,52 +15,52 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
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: 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
- name: Build project
run: pnpm build
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Test project
run: pnpm test
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- 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
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: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
+1 -1
View File
@@ -1 +1 @@
v20.10.0
22.11.0
+6 -6
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,
@@ -22,18 +22,18 @@ community include:
* 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
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.
+5 -5
View File
@@ -2,20 +2,20 @@
![](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
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
+4 -2
View File
@@ -6,7 +6,9 @@ members = [
"crates/tuono_lib_macros",
]
exclude = [
"examples/playground",
"apps/documentation",
"examples/mdx",
"examples/tuono",
"examples/tutorial"
"examples/tutorial",
"benches/tuono"
]
+47 -64
View File
@@ -1,98 +1,81 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
</p>
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
<p align="center">
⚠️ This project is under heavy development. API might drastically change ⚠️
</p>
<div align="center">
<img src="https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg" />
<img src="https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg" />
</div>
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
[✨Contributing](https://tuono.dev/documentation/contributing)
<br>
<br>
# Tuono
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
**Tuono is the fastest React framework out there**
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
- ⚙️ 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).
As already mentioned above, I strongly suggest you take a look at the
[tutorial](https://tuono.dev/documentation/tutorial).
Tuono is basically a CLI that provides all the commands to handle the fullstack project.
To download it you need [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.
To download and install it you just need to run `cargo install tuono`.
Then run `cargo install tuono`.
To create a new project run `tuono new [NAME]` (optionally you can pass the `--template` flag - check the
To list all the available commands, run `tuono -h`
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
Then to run the local development environment run inside the project folder `tuono dev`
Then, to run the local development environment, run it 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 to 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
+1
View File
@@ -0,0 +1 @@
# Stadalone apps folder
+106
View File
@@ -0,0 +1,106 @@
{
"root": true,
"reportUnusedDisableDirectives": true,
"ignorePatterns": ["**/build", "**/dist"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
],
"env": {
"browser": true,
"es2020": true,
},
"parserOptions": {
"tsconfigRootDir": ".",
"project": true,
"sourceType": "module",
"ecmaVersion": 2020,
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": true,
},
"react": {
"version": "detect",
},
},
"rules": {
"@typescript-eslint/array-type": "error",
"@typescript-eslint/no-wrapper-object-types": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{ "prefer": "type-imports" },
],
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/method-signature-style": ["error", "property"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
"custom": {
"regex": "^(T|T[A-Z][A-Za-z]+)$",
"match": true,
},
},
],
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-inferrable-types": [
"error",
{ "ignoreParameters": true },
],
"import/default": "error",
"import/export": "error",
"import/namespace": "error",
"import/newline-after-import": "error",
"import/no-cycle": "error",
"import/no-duplicates": "off",
"import/no-named-as-default-member": "error",
"import/no-unused-modules": "error",
"import/order": [
"off",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
],
},
],
"no-case-declarations": "error",
"no-empty": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-shadow": "error",
"no-undef": "off",
"sort-imports": "off",
},
}
+13
View File
@@ -0,0 +1,13 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.tuono
out
target
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.12.0"
glob = "0.3.1"
time = { version = "0.3", features = ["macros"] }
serde = { version = "1.0.202", features = ["derive"] }
+7
View File
@@ -0,0 +1,7 @@
# Tuono starter
This is the starter tuono project. To download it run in your terminal:
```shell
$ tuono new [NAME]
```
+31
View File
@@ -0,0 +1,31 @@
{
"name": "documentation",
"description": "The react/rust fullstack framework documentation",
"version": "0.0.1",
"private": true,
"scripts": {
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
"format": "prettier -u --write --ignore-unknown './src/**/*'",
"format:check": "prettier --check --ignore-unknown './src/**/*'",
"types": "tsc --noEmit"
},
"dependencies": {
"@mantine/code-highlight": "^7.11.2",
"@mantine/core": "^7.11.2",
"@mantine/hooks": "^7.11.2",
"@mdx-js/react": "^3.0.1",
"@tabler/icons-react": "^3.11.0",
"clsx": "^2.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "npm:tuono@0.12.0"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"postcss": "^8.4.39",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1"
}
}
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
@@ -0,0 +1,96 @@
import type { JSX } from 'react'
import { Breadcrumbs, Button } from '@mantine/core'
import { Link, Head } from 'tuono'
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
interface Breadcrumb {
href?: string
label: string
}
interface BreadcrumbsProps {
breadcrumbs: Breadcrumb[]
}
export default function TuonoBreadcrumbs({
breadcrumbs = [],
}: BreadcrumbsProps): JSX.Element {
return (
<>
<Head>
<script type="application/ld+json">
{`{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Tuono - The React/Rust fullstack framework",
"item": "https://tuono.dev"
},
{
"@type": "ListItem",
"position": 2,
"name": "Tuono - Documentation",
"item": "https://tuono.dev/documentation"
}${breadcrumbs.length > 0 ? ',' : ''}
${breadcrumbs
.map((br, i) =>
JSON.stringify({
'@type': 'ListItem',
position: i + 3,
name: br.label,
item: br.href ? `https://tuono.dev${br.href}` : undefined,
}),
)
.join(',')}]
}
`}
</script>
</Head>
<Breadcrumbs
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
mb="md"
mt="md"
>
<Button
href="/documentation"
component={Link}
variant="subtle"
radius="xl"
p={5}
>
<IconBolt />
</Button>
{breadcrumbs.map((br) => (
<BreadcrumbElement href={br.href} label={br.label} key={br.label} />
))}
</Breadcrumbs>
</>
)
}
interface BreadcrumbElementProps {
href?: string
label: string
}
export function BreadcrumbElement({
href,
label,
}: BreadcrumbElementProps): JSX.Element {
if (href) {
return (
<Button component={Link} href={href} variant="subtle" radius="xl" px={10}>
{label}
</Button>
)
}
return (
<Button component="span" variant="light" radius="xl">
{label}
</Button>
)
}
@@ -0,0 +1,5 @@
import Breadcrumbs, { BreadcrumbElement } from './breadcrumbs'
export default Breadcrumbs
export { BreadcrumbElement as Element }
@@ -0,0 +1,24 @@
import type { JSX } from 'react'
import { Button } from '@mantine/core'
import { IconEdit } from '@tabler/icons-react'
import { useRouter } from 'tuono'
const GITHUB_URL =
'https://github.com/Valerioageno/tuono/tree/main/apps/documentation/src/routes'
export default function EditPage(): JSX.Element {
const { pathname } = useRouter()
return (
<Button
p={0}
mt={60}
component="a"
variant="transparent"
leftSection={<IconEdit />}
target="_blank"
href={GITHUB_URL.concat(pathname).concat('.mdx')}
>
Edit page
</Button>
)
}
@@ -0,0 +1,3 @@
import EditPage from './edit-page'
export default EditPage
@@ -0,0 +1,54 @@
import type { JSX } from 'react'
import {
Button,
Center,
Container,
CopyButton,
Group,
rem,
Text,
Title,
} from '@mantine/core'
import { IconCopy, IconCheck } from '@tabler/icons-react'
import { Link } from 'tuono'
export default function Hero(): JSX.Element {
return (
<Container size={1100} my={100}>
<Center>
<Title order={1}>Superfast fullstack react framework</Title>
</Center>
<Center mt={50}>
<Text fz="18px">
The technologies we love seamlessly working together to unleash the
<strong> highest web performance</strong> ever met on React
</Text>
</Center>
<Center mt={50}>
<Group>
<Button component={Link} href="/documentation" size="lg">
Get Started
</Button>
<CopyButton value="cargo install tuono">
{({ copied, copy }) => (
<Button
onClick={copy}
size="lg"
style={{ border: 'solid 1px var(--mantine-color-violet-1)' }}
color="gray"
leftSection="$ cargo install tuono"
rightSection={
copied ? (
<IconCheck style={{ width: rem(20) }} />
) : (
<IconCopy style={{ width: rem(20) }} />
)
}
/>
)}
</CopyButton>
</Group>
</Center>
</Container>
)
}
@@ -0,0 +1,3 @@
import Hero from './hero'
export default Hero
@@ -0,0 +1,3 @@
import MdxProvider from './mdx-provider'
export default MdxProvider
@@ -0,0 +1,6 @@
import type { JSX } from 'react'
import { Text, type TextProps } from '@mantine/core'
export default function MdxBold(props: TextProps): JSX.Element {
return <Text fw={700} {...props} />
}
@@ -0,0 +1,3 @@
import MdxCode from './mdx-code'
export default MdxCode
@@ -0,0 +1,9 @@
import type { JSX } from 'react'
import { Code } from '@mantine/core'
import type { HTMLAttributes } from 'react'
export default function MdxCode(
props: HTMLAttributes<HTMLPreElement>,
): JSX.Element {
return <Code {...props} style={{ fontSize: 14 }} />
}
@@ -0,0 +1,3 @@
import MdxLink from './mdx-link'
export default MdxLink
@@ -0,0 +1,36 @@
import type { JSX, AnchorHTMLAttributes } from 'react'
import { Button } from '@mantine/core'
import { Link } from 'tuono'
import { IconExternalLink } from '@tabler/icons-react'
export default function MdxLink(
props: AnchorHTMLAttributes<HTMLAnchorElement>,
): JSX.Element {
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
return (
<Button
component="a"
{...props}
target="_blank"
rightSection={
<IconExternalLink size="16px" style={{ marginLeft: -5 }} />
}
variant="transparent"
style={{ height: '20px' }}
mt={-2}
p={0}
/>
)
}
return (
<Button
component={Link}
{...props}
target="_blank"
variant="transparent"
style={{ height: '20px' }}
mt={-2}
p={0}
/>
)
}
@@ -0,0 +1,3 @@
import MdxPre from './mdx-pre'
export default MdxPre
@@ -0,0 +1,5 @@
.pre {
code {
font-size: 14px;
}
}
@@ -0,0 +1,17 @@
import type { JSX } from 'react'
import { CodeHighlight } from '@mantine/code-highlight'
import styles from './mdx-pre.module.css'
interface PreProps {
children: any
}
export default function MdxPre({ children }: PreProps): JSX.Element {
return (
<CodeHighlight
className={styles.pre}
code={children.props.children || ''}
style={{ borderRadius: 8, fontWeight: 100 }}
language={children.props.className?.replace('language-', '')}
/>
)
}
@@ -0,0 +1,39 @@
import type { JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
import MdxLink from './mdx-link'
import type { ReactNode } from 'react'
import MdxPre from './mdx-pre'
import MdxQuote from './mdx-quote'
import MdxCode from './mdx-code'
import { h } from './mdx-title'
import MdxBold from './mdx-bold/mdx-bold'
interface MdxProviderProps {
children: ReactNode
}
export default function MdxProvider({
children,
}: MdxProviderProps): JSX.Element {
return (
<MDXProvider
components={{
a: MdxLink,
// @ts-expect-error: useless finding the correct props types
pre: MdxPre,
blockquote: MdxQuote,
code: MdxCode,
h1: h(1),
h2: h(2),
h3: h(3),
h4: h(4),
h5: h(5),
h6: h(6),
strong: MdxBold,
}}
>
{children}
</MDXProvider>
)
}
@@ -0,0 +1,3 @@
import MdxQuote from './mdx-quote'
export default MdxQuote
@@ -0,0 +1,21 @@
import type { JSX, HTMLAttributes } from 'react'
import { Blockquote, Space } from '@mantine/core'
export default function MdxQuote(
props: HTMLAttributes<HTMLQuoteElement>,
): JSX.Element {
return (
<>
<Blockquote
color="violet"
py={1}
px={20}
mt={30}
iconSize={30}
{...props}
style={{ borderRadius: 8 }}
/>
<Space h="md" />
</>
)
}
@@ -0,0 +1,5 @@
import MdxTitle, { h } from './mdx-title'
export default MdxTitle
export { h }
@@ -0,0 +1,20 @@
import type { JSX } from 'react'
import { Title, type TitleProps } from '@mantine/core'
export default function MdxTitle(props: TitleProps): JSX.Element {
return (
<Title
data-heading={props.children}
data-order={props.order}
mt={20}
{...props}
id={String(props.children ?? '')
.toLowerCase()
.replaceAll(' ', '-')}
/>
)
}
export const h =
(order: 1 | 2 | 3 | 4 | 5 | 6) =>
(props: TitleProps): JSX.Element => <MdxTitle order={order} {...props} />
@@ -0,0 +1,3 @@
import MetaTags from './meta-tags'
export default MetaTags
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react'
import { Head } from 'tuono'
interface MetaTagsProps {
title: string
description?: string
type?: 'website' | 'article' | 'book' | 'video.movie' | 'music.song'
canonical: string
}
export default function MetaTags({
title,
description,
type = 'article',
canonical,
}: MetaTagsProps): ReactNode {
return (
<Head>
<title>{title}</title>
<link rel="canonical" href={canonical} />
<meta
name="description"
content={
description ??
'The technologies we love seamlessly working together to unleash the highest web performance ever met on React'
}
/>
<meta
name="keywords"
content="ReactJs, Typescript, Rust, JavaScript, Fullstack framework, Web development"
/>
<meta property="og:title" content={title} />
<meta property="og:type" content={type} />
<meta property="og:image" content="https://tuono.dev/og-cover.png" />
<meta property="og:url" content={canonical} />
<meta property="og:description" content={description} />
<meta property="og:site_name" content="Tuono" />
<meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@tuono_dev" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="https://tuono.dev/og-cover.png" />
</Head>
)
}
@@ -0,0 +1,45 @@
import type { JSX } from 'react'
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
import { IconBrandGithub, IconBook, IconBrandX } 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">
<Button
href="/documentation"
component={Link}
size="compact-lg"
rightSection={<IconBook />}
autoContrast
>
Get started
</Button>
<ActionIcon
variant="default"
size="lg"
aria-label="Check the project on github"
href="https://github.com/Valerioageno/tuono"
target="_blank"
component="a"
>
<IconBrandGithub />
</ActionIcon>
<ActionIcon
variant="default"
size="lg"
aria-label="We are also on X!"
href="https://x.com/tuono_dev"
target="_blank"
component="a"
>
<IconBrandX />
</ActionIcon>
</Group>
<ThemeBtn />
</Flex>
)
}
@@ -0,0 +1,3 @@
import Navbar from './navbar'
export default Navbar
@@ -0,0 +1,34 @@
import type { JSX } from 'react'
import { AppShell, Burger, Button, Flex } from '@mantine/core'
import { Link, useRouter } from 'tuono'
import Actions from './actions'
interface NavbarProps {
opened: boolean
toggle: () => void
}
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
const { pathname } = useRouter()
return (
<AppShell.Header p="sm">
<Flex justify="space-between">
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
Tuono
</Button>
<Flex align="center" gap={8}>
<Actions />
{pathname.startsWith('/documentation') && (
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
)}
</Flex>
</Flex>
</AppShell.Header>
)
}
@@ -0,0 +1,3 @@
import NavigationButtons from './navigation-buttons'
export default NavigationButtons
@@ -0,0 +1,63 @@
import type { JSX } from 'react'
import { Box, Button, Text, Title, Flex } from '@mantine/core'
import { Link } from 'tuono'
import { IconArrowRight, IconArrowLeft } from '@tabler/icons-react'
interface NavigationButton {
href: string
title: string
}
interface NavigationButtonsProps {
prev?: NavigationButton
next?: NavigationButton
}
export default function NavigationButtons({
prev,
next,
}: NavigationButtonsProps): JSX.Element {
return (
<Flex mt={50} gap={10}>
{prev && <NavigationBtn type="prev" {...prev} />}
{next && <NavigationBtn type="next" {...next} />}
</Flex>
)
}
interface NavigationButtonProps extends NavigationButton {
type: 'next' | 'prev'
}
const NavigationBtn = ({
type,
title,
href,
}: NavigationButtonProps): JSX.Element => {
const heading = type === 'next' ? 'Next' : 'Previous'
const textAlign = type === 'next' ? 'left' : 'right'
const variant = type === 'next' ? 'filled' : 'outline'
return (
<Button
component={Link}
fullWidth
variant={variant}
href={href}
justify="space-between"
h="auto"
rightSection={type === 'next' && <IconArrowRight />}
leftSection={type === 'prev' && <IconArrowLeft />}
p="20"
>
<Box>
<Title component="span" display="block" order={4} style={{ textAlign }}>
{heading}
</Title>
<Text component="span" display="block" style={{ textAlign }}>
{title}
</Text>
</Box>
</Button>
)
}
@@ -0,0 +1,3 @@
import Sidebar from './sidebar'
export default Sidebar
@@ -0,0 +1,6 @@
.link {
border-radius: 8px;
margin-top: 0.25rem;
line-height: 1.25rem;
font-weight: 500;
}
@@ -0,0 +1,47 @@
import type { JSX, ReactNode } from 'react'
import { useState } from 'react'
import { NavLink, type NavLinkProps } from '@mantine/core'
import { Link, useRouter } from 'tuono'
import { IconChevronRight } from '@tabler/icons-react'
import styles from './sidebar-link.module.css'
interface SidebarLinkProps {
label: string
href: string
onClick?: () => void
children?: ReactNode
}
export default function SidebarLink(
props: SidebarLinkProps & NavLinkProps,
): JSX.Element {
const { pathname } = useRouter()
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
const internalProps = {
active: pathname === props.href,
className: styles.link,
rightSection: props.children && (
<IconChevronRight
size="1.2rem"
stroke={1.5}
className="mantine-rotate-rtl"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setIsOpen((state) => !state)
}}
/>
),
opened: isOpen,
autoContrast: true,
...props,
}
if (props.href.startsWith('#')) {
return <NavLink component="button" {...internalProps} />
}
return <NavLink component={Link} {...internalProps} />
}
@@ -0,0 +1,91 @@
import type { JSX } from 'react'
import { AppShell } from '@mantine/core'
import SidebarLink from './sidebar-link'
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
return (
<AppShell.Navbar p="md">
<SidebarLink
href="/documentation"
label="Getting started"
onClick={close}
/>
<SidebarLink
href="/documentation/installation"
label="Installation"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial"
label="Tutorial"
defaultOpened
onClick={close}
>
<SidebarLink
href="/documentation/tutorial/development-setup"
label="Development setup"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/api-fetching"
label="API fetching"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/components"
label="Components"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/dynamic-routes"
label="Dynamic routes"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/error-handling"
label="Error handling"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/seo"
label="SEO and meta tags"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/redirections"
label="Server redirection"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/production"
label="Production build"
onClick={close}
/>
<SidebarLink
href="/documentation/tutorial/conclusion"
label="Conclusion"
onClick={close}
/>
</SidebarLink>
<SidebarLink href="/documentation/cli" label="CLI" onClick={close} />
<SidebarLink
label="Routing"
href="/documentation/routing"
onClick={close}
>
<SidebarLink
href="/documentation/routing/intro"
label="Project structure"
onClick={close}
/>
</SidebarLink>
<SidebarLink
label="Contributing"
href="/documentation/contributing"
onClick={close}
leftSection="✨"
/>
</AppShell.Navbar>
)
}
@@ -0,0 +1,38 @@
/**
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
*/
export interface Heading {
depth: number
content: string
id: string
getNode: () => HTMLHeadingElement
}
function getHeadingsData(headings: HTMLHeadingElement[]): Heading[] {
const result: Heading[] = []
for (const heading of headings) {
if (heading.id) {
result.push({
depth: parseInt(heading.getAttribute('data-order') || '1', 10),
content: heading.getAttribute('data-heading') || '',
id: heading.id,
getNode: () =>
document.getElementById(heading.id) as HTMLHeadingElement,
})
}
}
return result
}
export function getHeadings(): Heading[] {
const root = document.getElementById('mdx-root')
console.log(root)
if (!root) {
return []
}
return getHeadingsData(Array.from(root.querySelectorAll('[data-heading]')))
}
@@ -0,0 +1,6 @@
/*
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
*/
import { TableOfContents } from './table-of-content'
export default TableOfContents
@@ -0,0 +1,120 @@
/*
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
*/
.wrapper {
padding-left: var(--mantine-spacing-md);
position: sticky;
top: var(--mantine-spacing-xl);
right: 0;
padding-top: 55px;
flex: 0 0 calc(var(--docs-table-of-contents-width) - 20px);
width: 200px;
@mixin rtl {
padding-left: 0;
padding-right: var(--mantine-spacing-md);
right: auto;
left: 0;
}
&[data-with-tabs] {
padding-top: 0;
top: calc(var(--mantine-spacing-xl) + 60px);
}
}
.inner {
padding-bottom: var(--mantine-spacing-xl);
padding-left: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
justify-content: space-between;
@mixin rtl {
padding-left: 0;
padding-right: var(--mantine-spacing-md);
}
}
.link {
display: block;
border-left: 1px solid transparent;
padding: 8px var(--mantine-spacing-md);
margin-left: -1px;
padding-left: calc(var(--toc-link-offset) * var(--mantine-spacing-lg));
border-top-right-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
@mixin light {
color: var(--mantine-color-gray-7);
}
@mixin dark {
color: var(--mantine-color-dark-1);
}
@mixin rtl {
border-left: 0;
border-right: 1px solid transparent;
margin-left: 0;
margin-right: -1px;
border-top-left-radius: var(--mantine-radius-sm);
border-bottom-left-radius: var(--mantine-radius-sm);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&[data-active] {
border-color: var(--mantine-color-violet-5);
@mixin light {
color: var(--mantine-color-violet-8);
background-color: var(--mantine-color-violet-0);
}
@mixin dark {
color: var(--mantine-color-violet-1);
background-color: #534a6d;
}
}
}
.header {
display: flex;
align-items: center;
margin-bottom: var(--mantine-spacing-md);
}
.title {
margin-left: var(--mantine-spacing-md);
@mixin rtl {
margin-left: 0;
margin-right: var(--mantine-spacing-md);
}
}
.items {
border-left: 1px solid;
@mixin light {
border-color: var(--mantine-color-gray-3) !important;
}
@mixin dark {
border-color: var(--mantine-color-dark-4) !important;
}
@mixin rtl {
border-left: 0;
border-right: 1px solid;
@mixin light {
border-color: var(--mantine-color-gray-3) !important;
}
@mixin dark {
border-color: var(--mantine-color-dark-4) !important;
}
}
}
@@ -0,0 +1,114 @@
/*
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
*/
import type { JSX } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useRouter, Link } from 'tuono'
import { IconList } from '@tabler/icons-react'
import { Box, rem, ScrollArea, Text } from '@mantine/core'
import { getHeadings, type Heading } from './get-headings'
import classes from './table-of-content.module.css'
interface TableOfContentsProps {
withTabs: boolean
}
function getActiveElement(rects: DOMRect[]): number {
if (rects.length === 0) {
return -1
}
const closest = rects.reduce(
(acc, item, index) => {
if (Math.abs(acc.position) < Math.abs(item.y)) {
return acc
}
return {
index,
position: item.y,
}
},
{ index: 0, position: rects[0].y },
)
return closest.index
}
export function TableOfContents({
withTabs,
}: TableOfContentsProps): JSX.Element | null {
const [active, setActive] = useState(0)
const [headings, setHeadings] = useState<Heading[]>([])
const headingsRef = useRef<Heading[]>([])
const router = useRouter()
const filteredHeadings = headings.filter((heading) => heading.depth > 1)
const handleScroll = (): void => {
setActive(
getActiveElement(
headingsRef.current.map((d) => d.getNode().getBoundingClientRect()),
),
)
}
useEffect(() => {
const _headings = getHeadings()
headingsRef.current = _headings
setHeadings(_headings)
setActive(
getActiveElement(
_headings.map((d) => d.getNode().getBoundingClientRect()),
),
)
window.addEventListener('scroll', handleScroll)
return (): void => window.removeEventListener('scroll', handleScroll)
}, [router.pathname])
if (filteredHeadings.length === 0) {
return null
}
const items = filteredHeadings.map((heading, index) => (
<Text
key={heading.id}
component={Link}
fz="sm"
p={10}
className={classes.link}
mod={{ active: active === index }}
href={`#${heading.id}`}
__vars={{ '--toc-link-offset': `${heading.depth - 1}` }}
>
{heading.content}
</Text>
))
return (
<Box
component="nav"
mod={{ 'with-tabs': withTabs }}
className={classes.wrapper}
>
<div className={classes.inner}>
<div>
<div className={classes.header}>
<IconList
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
<Text className={classes.title}>Table of contents</Text>
</div>
<ScrollArea.Autosize
mah={`calc(100vh - ${rem(140)})`}
type="never"
offsetScrollbars
>
<div className={classes.items}>{items}</div>
</ScrollArea.Autosize>
</div>
</div>
</Box>
)
}
@@ -0,0 +1,3 @@
import ThemeBtn from './theme-btn'
export default ThemeBtn
@@ -0,0 +1,24 @@
.icon {
width: 22px;
height: 22px;
}
.dark {
@mixin dark {
display: none;
}
@mixin light {
display: block;
}
}
.light {
@mixin light {
display: none;
}
@mixin dark {
display: block;
}
}
@@ -0,0 +1,31 @@
import type { JSX } from 'react'
import {
ActionIcon,
useMantineColorScheme,
useComputedColorScheme,
} from '@mantine/core'
import { IconSun, IconMoon } from '@tabler/icons-react'
import cx from 'clsx'
import classes from './theme-btn.module.css'
export default function ThemeBtn(): JSX.Element {
const { setColorScheme } = useMantineColorScheme()
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
})
return (
<ActionIcon
onClick={() =>
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
}
variant="default"
size="lg"
aria-label="Toggle color scheme"
>
<IconSun className={cx(classes.icon, classes.light)} stroke={1.5} />
<IconMoon className={cx(classes.icon, classes.dark)} stroke={1.5} />
</ActionIcon>
)
}
+2
View File
@@ -0,0 +1,2 @@
// declaration.d.ts
declare module '*.css'
+107
View File
@@ -0,0 +1,107 @@
import type { ReactNode, JSX } from 'react'
import {
ColorSchemeScript,
createTheme,
MantineProvider,
AppShell,
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
import { Head, useRouter } from 'tuono'
import Navbar from '../components/navbar'
import '@mantine/core/styles.css'
import '@mantine/code-highlight/styles.css'
import Sidebar from '../components/sidebar'
interface RootRouteProps {
children: ReactNode
}
const theme = createTheme({
primaryColor: 'violet',
primaryShade: { light: 6, dark: 9 },
fontFamily: 'Roboto',
respectReducedMotion: true,
radius: {
xs: '8px',
lg: '8px',
xl: '8px',
md: '8px',
sm: '8px',
},
fontSizes: {
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
xs: '16px',
sm: '16px',
},
colors: {
dark: [
'#d5d7e0',
'#acaebf',
'#8c8fa3',
'#666980',
'#4d4f66',
'#34354a',
'#2b2c3d',
'#1d1e30',
'#0c0d21',
'#01010a',
],
},
headings: {
sizes: {
h1: {
fontSize: '48px',
},
},
},
})
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
const [opened, { toggle }] = useDisclosure()
const { pathname } = useRouter()
return (
<>
<Head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</Head>
<ColorSchemeScript />
<MantineProvider theme={theme}>
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: 'sm',
collapsed: { mobile: !opened },
}}
>
<Navbar opened={opened} toggle={toggle} />
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
{children}
</AppShell>
</MantineProvider>
</>
)
}
@@ -0,0 +1,20 @@
import type { ReactNode, JSX } from 'react'
import { AppShell, Container } from '@mantine/core'
import MdxProvider from '../../components/mdx-provider'
import EditPage from '../../components/edit-page'
interface RootRouteProps {
children: ReactNode
}
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
return (
<AppShell.Main>
<Container id="mdx-root" component="article" size="md" p={20}>
<MdxProvider>{children}</MdxProvider>
<EditPage />
</Container>
</AppShell.Main>
)
}
@@ -0,0 +1,44 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - CLI"
canonical="https://tuono.dev/documentation/cli"
description="Tuono is the CLI that provides all the needed commands to handle the full-stack project."
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
# CLI
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
> `tuono` CLI yet.
To list all the available commands, run `tuono -h`.
```bash
The React/Rust full-stack framework
Usage: tuono <COMMAND>
Commands:
dev Start the development environment
build Build the production assets
new Scaffold a new project
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
```
To create a new project, run `tuono new [NAME]` (optionally you can pass the --template (or -t) flag - check the examples' folder).
Then to run the local development environment run inside the project folder `tuono dev`
Finally, when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
Now to execute it, just run `cargo run --release`.
@@ -0,0 +1,48 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Contributing"
canonical="https://tuono.dev/documentation/contributing"
description="The project is massive - if you like it, do consider contributing!"
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
# Contributing
## TL;DR
The project is massive - if you like it, do consider contributing!
## Getting started
The `tuono` project can mostly be split into the following subdomains:
- The CLI
- The Rust backend
- The React frontend
- The documentation website (which is written with tuono 🚀)
To check what the knowledge requirements are for each domain, check the
[requirements](#requirements) section below.
Currently, I'm keeping a private dashboard to prioritize new features and bug fixes, but if you
want to propose something, please open a new issue on Github or reach out to me using
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
on Twitter (X) DMs `@valerioageno`, [Linkedin](https://www.linkedin.com/in/valerioageno)
and discord `@__v__v__`.
## Requirements
It's not strictly required to know both React (& typescript) and Rust (even though it
would be a great nice to have).
Without taking into account specific cases, we can mostly split the domain requirements by:
- The `CLI` needs Rust knowledge (even though a couple of scenarios might also need Typescript)
- The Backend needs just `Rust`
- The Frontend needs just `React` & `Typescript`
- The documentation website needs just `React` & `Typescript` (or even less, since most of the
code is markdown).
@@ -0,0 +1,54 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Documentation"
canonical="https://tuono.dev/documentation"
description="Tuono is a full-stack framework for building React applications using Rust as the backend."
/>
import Breadcrumbs from '../../components/breadcrumbs'
<Breadcrumbs />
# Tuono
Tuono is a full-stack web framework for building React applications using Rust as the
backend with a strong focus on usability and performance.
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
## Features
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
## Introduction
Tuono addresses the following two limits:
- Enable React server-side rendering projects without using JS runtimes (Node, Deno)
- Ease the web development using a typically hard language like Rust.
#### NodeJs/Deno/Bun are the only runtimes that allow a React app to be server-side rendered (SSR)? (no)
Tuono is the first full-stack React framework with the server side written in Rust (check the [ssr-rs](https://github.com/Valerioageno/ssr-rs) crate).
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
React is still React - it is just superpowered.
#### Rust is a hard language, then writing server side code is hard as well, right? (no again)
Tuono provides a collection of utilities to handle server-side code seamlessly with
the React code. Each server side route is managed in a separate file alongside the React route.
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
#### How is Tuono different from Next.js?
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
@@ -0,0 +1,57 @@
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Installation"
canonical="https://tuono.dev/documentation/installation"
description="Tuono is a development environment built in rust and typescript that outputs a website written in both the languages"
/>
<Breadcrumbs breadcrumbs={[{ label: 'Installation' }]} />
# Installation
## Requirements
`Tuono` is a development environment built in rust and typescript that outputs a website written in both the
languages. Since then, you need the following tools installed on your computer to work with it:
- `Rust` - The Rust programming language tool chain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
- `Cargo` - The rust package manager
- `NodeJs` - The JavaScript runtime (Go [here](https://nodejs.org/en/download/package-manager) for installing both `nodejs` and `npm`)
- `npm` or `yarn` or `pnpm` - A javaScript package manager
> Node.js is needed just for the development environment. The final output will run on a Rust server.
## Installation
The tuono `CLI` is hosted on [crates.io](https://crates.io/crates/tuono) to download and install it, just run it on a terminal:
```bash
cargo install tuono
```
To check that it is correctly installed, run:
```bash
tuono --version
```
Run `tuono -h` to see all the available commands.
```bash
The React/Rust full-stack framework
Usage: tuono <COMMAND>
Commands:
dev Start the development environment
build Build the production assets
new Scaffold a new project
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
```
@@ -0,0 +1,14 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing/intro"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
# Routing index
TODO
@@ -0,0 +1,19 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs
breadcrumbs={[
{ label: 'Routing', href: '/documentation/routing' },
{ label: 'Project structure' },
]}
/>
# Project structure
Todo
@@ -0,0 +1,124 @@
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 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("{}")),
};
}
```
> The first argument is always the request `req: Request` which contains the request data like the query parameters and the headers.
> The rest of the arguments are optional and they don't need to be specified
> if they are not used. Enabled out of the box a [Reqwest](https://docs.rs/reqwest/latest/reqwest/) HTTP client.
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 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 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 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,
)),
};
}
```
<br />
```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 Pokémon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
correctly receive a 404 status code in the console.
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
prev={{
title: 'Dynamic routes',
href: '/documentation/tutorial/dynamic-routes',
}}
next={{ title: 'SEO and meta tags', href: '/documentation/tutorial/seo' }}
/>
@@ -0,0 +1,37 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Tutorial"
canonical="https://tuono.dev/documentation/tutorial"
description="This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the development - be sure to have installed the latest version"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
# Tutorial
This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the
development - be sure to have [installed](/documentation/installation) the [latest version](https://crates.io/crates/tuono).
This tutorial is not meant for people who don't know React - in that case, I suggest you first read the
[React Doc](https://react.dev/); Typescript and Rust knowledge is not a requirement though!
If you prefer to just read the code rather than write you can download the finished tutorial project with:
```bash
$ tuono new tutorial --template tutorial
```
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it)
> or on Twitter (X) DM [@valerioageno](https://twitter.com/valerioageno)
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
next={{
title: 'Development setup',
href: '/documentation/tutorial/development-setup',
}}
/>
@@ -0,0 +1,47 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Building for production"
canonical="https://tuono.dev/documentation/tutorial/production"
description="Learn how to build the production project and run the optimize version"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Production build' },
]}
/>
# Production build
The source is now ready to be released. Both server and client have been managed in an unoptimized way
to ease the development experience. To build the project to the production state, just run:
```shell
$ tuono build
```
This command just created the final assets within the `out` directory. To run the production server,
run:
```shell
$ cargo run --release
```
Check again [`http://localhost:3000/`](http://localhost:3000/) This environment now has all the
optimizations ready to unleash the power of a rust server that seamlessly renders a React application!🚀
> Note: The `out` directory is not standalone. You can't rely just on it to run the production server.
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
prev={{
title: 'Server redirection',
href: '/documentation/tutorial/redirections',
}}
next={{ title: 'Conclusion', href: '/documentation/tutorial/conclusion' }}
/>
@@ -0,0 +1,61 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Redirection"
canonical="https://tuono.dev/documentation/tutorial/redirection"
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/eeo"
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',
}}
/>
+17
View File
@@ -0,0 +1,17 @@
import type { JSX } from 'react'
import Hero from '../components/hero'
import MetaTags from '../components/meta-tags'
export default function IndexPage(): JSX.Element {
return (
<>
<MetaTags
title="Tuono - The React/Rust full-stack framework"
canonical="https://tuono.dev"
description="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
/>
<Hero />
</>
)
}
@@ -0,0 +1,61 @@
use tuono_lib::reqwest::StatusCode;
use tuono_lib::{Request, Response};
use tuono_lib::axum::http::{header, HeaderMap};
use glob::glob;
use time::OffsetDateTime;
const FILE_TO_EXCLUDE: [&str; 2] = ["sitemap.xml", "__root"];
const SITEMAP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
[PLACEHOLDER]
</urlset>"#;
fn load_routes() -> Vec<String> {
let mut paths: Vec<String> = vec![];
for entry in glob("./src/routes/**/*").expect("Failed to glob src/routes folder").flatten() {
if !entry.is_dir() {
let path = clean_path(format!("/{}", entry.to_string_lossy()));
if !FILE_TO_EXCLUDE.iter().any(|exclude| path.ends_with(exclude)) {
paths.push(path)
}
}
}
paths
}
fn clean_path(value: String) -> String {
value
.replace("src/routes/", "")
.replace(".mdx", "")
.replace(".tsx", "")
.replace(".rs", "")
.replace("index", "")
}
#[tuono_lib::handler]
async fn generate_sitemap(_req: Request) -> Response {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/xml".parse().unwrap());
let routes = load_routes();
let mut sitemaps = String::new();
for path in routes {
let mut url = format!("https://tuono.dev{}", path);
if url.ends_with('/') {
url.pop();
}
sitemaps.push_str(
&format!(r#"<url><loc>{}</loc><lastmod>{}</lastmod></url>"#,url, OffsetDateTime::now_utc().date())
)
}
Response::Custom((StatusCode::OK, headers, SITEMAP.replace("[PLACEHOLDER]", &sitemaps)))
}
+1
View File
@@ -0,0 +1 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+83
View File
@@ -0,0 +1,83 @@
# Benchmarks
This folder includes multiple setups to compare the **HTTP performance** against `tuono` and other frameworks.
The main goal is to make the comparison as fair as possible enhancing each framework specific
performance improvements.
Any improvement to the benchmarks or implementation of a different framework is strongly appreciated.
Feel free also to try it with your hardware and open an issue in case you spot significant different results.
## Benchmark setup
In order to make each comparison as fair as possible each framework should contain a single server side
rendered page that requires data from the backend service.
The data returned by the backend service should be a JSON including a random number between 0 and 10.
```json
{
"data": 0
}
```
Then the benchmark is triggered by running the production server and query the `/` endpoint with [wrk](https://github.com/wg/wrk)
to mock an heavy network load over the application server.
## Results
```
// Tuono
> tuono build
> cargo run --release
> wrk -t12 -c400 -d30s http://localhost:3000/
Running 30s test @ http://localhost:3000/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 79.72ms 162.90ms 1.71s 87.70%
Req/Sec 11.50k 15.04k 42.40k 67.36%
4111435 requests in 30.06s, 3.03GB read
Requests/sec: 136788.14
Transfer/sec: 103.08MB
```
```
// NextJs pages router
> pnpm build
> pm2 start ./ecosystem.config.js
> wrk -t12 -c400 -d30s http://localhost:3000/
Running 30s test @ http://localhost:3000/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 36.02ms 21.81ms 608.77ms 93.99%
Req/Sec 0.96k 183.53 3.38k 84.99%
344202 requests in 30.10s, 493.73MB read
Requests/sec: 11434.43
Transfer/sec: 16.40MB
```
```
// NextJs app router
> pnpm build
> pm2 start ./ecosystem.config.js
> wrk -t12 -c400 -d30s http://localhost:3000/
Running 30s test @ http://localhost:3000/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 61.03ms 18.65ms 341.31ms 86.25%
Req/Sec 547.55 96.59 1.16k 80.59%
195590 requests in 30.10s, 715.91MB read
Requests/sec: 6498.62
Transfer/sec: 23.79MB
```
+36
View File
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
@@ -0,0 +1,10 @@
module.exports = {
apps: [
{
script: 'node_modules/next/dist/bin/next',
args: 'start',
instances: 'max',
exec_mode: 'cluster',
},
],
}
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
+464
View File
@@ -0,0 +1,464 @@
{
"name": "nextjs-app-router",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nextjs-app-router",
"version": "0.1.0",
"dependencies": {
"next": "14.2.5",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"typescript": "^5"
}
},
"node_modules/@next/env": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
"integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA=="
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
"integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
"integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
"integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
"integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
"integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
"integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
"integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
"integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
"integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "14.2.5",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
"integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
"dependencies": {
"@next/env": "14.2.5",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.5",
"@next/swc-darwin-x64": "14.2.5",
"@next/swc-linux-arm64-gnu": "14.2.5",
"@next/swc-linux-arm64-musl": "14.2.5",
"@next/swc-linux-x64-gnu": "14.2.5",
"@next/swc-linux-x64-musl": "14.2.5",
"@next/swc-win32-arm64-msvc": "14.2.5",
"@next/swc-win32-ia32-msvc": "14.2.5",
"@next/swc-win32-x64-msvc": "14.2.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "nextjs-app-router",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"react": "^18",
"react-dom": "^18",
"next": "14.2.5"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18"
}
}

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