Compare commits

..

24 Commits

Author SHA1 Message Date
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
85 changed files with 829 additions and 458 deletions
+2 -1
View File
@@ -49,7 +49,7 @@
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
@@ -59,6 +59,7 @@
},
},
],
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
-23
View File
@@ -1,23 +0,0 @@
## Description
A clear and concise description of what the bug is.
## Expected behaviour
A clear and concise description of what you expected to happen.
## How to reproduce
Steps to reproduce the behavior:
## Screenshots
If applicable, add screenshots to help explain your problem.
## Tooling and environment
- Tuono version [e.g. 0.4.0]
- OS: [e.g. MacOS, Windows]
- Browser: [e.g. chrome, safari]
- Node version: [e.g. 20.0.0]
- Rust version: [e.g. 1.79.0]
- Crate version: [e.g. 1.78.0]
- Node Package Manger version: [e.g. pnpm: 9.5.0]
## Additional context
Add any other context about the problem here.
+90
View File
@@ -0,0 +1,90 @@
name: Bug report
description: File a bug report
title: '[Bug]: '
labels: ['bug']
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: steps
attributes:
label: How to reproduce
description: Steps to reproduce the behavior (code, example repository)
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: input
id: tuono-version
attributes:
label: Tuono version
placeholder: "[e.g. 0.4.0]"
validations:
required: false
- type: input
id: os-version
attributes:
label: OS
placeholder: "[e.g. MacOS, Windows]"
validations:
required: false
- type: input
id: browser-version
attributes:
label: Browser
placeholder: "[e.g. chrome, safari]"
validations:
required: false
- type: input
id: node-version
attributes:
label: Node version
placeholder: "[e.g. 20.0.0]"
validations:
required: false
- type: input
id: rust-version
attributes:
label: Rust version
placeholder: "[e.g. 1.79.0]"
validations:
required: false
- type: input
id: create-version
attributes:
label: Crate version
placeholder: "[e.g. 1.78.0]"
validations:
required: false
- type: input
id: node-package-manager-version
attributes:
label: Node Package Manger version
placeholder: "[e.g. pnpm: 9.5.0] "
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the bug here.
validations:
required: false
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,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
+6 -11
View File
@@ -15,9 +15,6 @@ 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
defaults:
run:
working-directory: ./apps/documentation
steps:
- name: Check out code
@@ -40,18 +37,16 @@ jobs:
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
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
defaults:
run:
working-directory: ./apps/documentation
steps:
- name: Check out code
@@ -63,13 +58,13 @@ jobs:
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
run: pnpm install
- name: Check formatting
run: pnpm format:check
run: pnpm docs:format:check
- name: Lint
run: pnpm lint
run: pnpm docs:lint
- name: Types
run: pnpm types
run: pnpm docs:types
+3 -4
View File
@@ -10,9 +10,7 @@ on:
jobs:
deploy-documentation:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./apps/documentation
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
@@ -37,9 +35,10 @@ jobs:
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --ignore-workspace
run: pnpm install
- name: Build project
working-directory: ./apps/documentation
run: tuono build --static
- name: Deploy
+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
+2 -2
View File
@@ -6,14 +6,14 @@
## Contributions
Any feature contribution or suggestion is strongly appreciated.
Since the current project size, there isn't yet a defined way to start a discussion. Consider [opening a new issue](https://github.com/Valerioageno/tuono/issues/new)
Since the current project size, there isn't yet a defined way to start a discussion. Consider [opening a new issue](https://github.com/Valerioageno/tuono/issues/new/choose)
or to reach me using my email address, [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on Twitter (X); DM @valerioageno.
## Bugs
**Did you find a bug?**
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Valerioageno/tuono/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
**Did you write a patch that fixes a bug?**
- Open a new GitHub pull request with the patch.
+21 -11
View File
@@ -10,16 +10,18 @@
**Tuono is the fastest React framework out there**
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
Some of its features are:
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
- 🟦 Native Typescript
- 🌐 NextJs like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
Some of its features are:
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
## 📖 Documentation
@@ -30,7 +32,7 @@ The [documentation](https://tuono.dev/documentation) is available on
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)**
Tuono is a full-stack React framework with the server side written in Rust.
Tuono is a full-stack React framework with the server side written in Rust.
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
React is still React - it is just superpowered.
@@ -40,12 +42,19 @@ Tuono provides a collection of utilities to handle the server side code seamless
Each server-side route is managed in a separate file alongside the React route. The routing is handled
by Tuono based on the files defined within the `./src/routes` directory.
**How is Tuono different from Next.js?**
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
## Getting started
As already mentioned above, I strongly suggest you take a look at the
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
[tutorial](https://tuono.dev/documentation/tutorial).
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
package manager.
@@ -53,7 +62,7 @@ Then run `cargo install tuono`.
To list all the available commands, run `tuono -h`
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
Then, to run the local development environment, run it inside the project folder `tuono dev`
@@ -64,6 +73,7 @@ and to set the server project in `production` mode.
Now to execute it just run `cargo run --release`.
## Contributing
Any help or suggestion will be appreciated and encouraged.
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
+2 -1
View File
@@ -49,7 +49,7 @@
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
@@ -59,6 +59,7 @@
},
},
],
"@typescript-eslint/no-deprecated": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
+1 -1
View File
@@ -8,7 +8,7 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.11.0"
tuono_lib = "0.12.0"
glob = "0.3.1"
time = { version = "0.3", features = ["macros"] }
serde = { version = "1.0.202", features = ["derive"] }
+5 -14
View File
@@ -1,7 +1,8 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"name": "documentation",
"description": "The react/rust fullstack framework documentation",
"version": "0.0.1",
"private": true,
"scripts": {
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
"format": "prettier -u --write --ignore-unknown './src/**/*'",
@@ -17,24 +18,14 @@
"clsx": "^2.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "0.11.0"
"tuono": "npm:tuono@0.12.0"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^5.0.0",
"postcss": "^8.4.39",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.4",
"typescript": "^5.5.4"
"postcss-simple-vars": "^7.0.1"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

@@ -1,31 +1,73 @@
import type { JSX } from 'react'
import { Breadcrumbs, Button } from '@mantine/core'
import { Link } from 'tuono'
import { Link, Head } from 'tuono'
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
import type { ReactNode } from 'react'
interface Breadcrumb {
href?: string
label: string
}
interface BreadcrumbsProps {
breadcrumbs: Breadcrumb[]
}
export default function TuonoBreadcrumbs({
children,
}: {
children: ReactNode
}): JSX.Element {
breadcrumbs = [],
}: BreadcrumbsProps): JSX.Element {
return (
<Breadcrumbs
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
mb="md"
mt="md"
>
<Button
href="/documentation"
component={Link}
variant="subtle"
radius="xl"
p={5}
<>
<Head>
<script type="application/ld+json">
{`{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Tuono - The React/Rust fullstack framework",
"item": "https://tuono.dev"
},
{
"@type": "ListItem",
"position": 2,
"name": "Tuono - Documentation",
"item": "https://tuono.dev/documentation"
}${breadcrumbs.length > 0 ? ',' : ''}
${breadcrumbs
.map((br, i) =>
JSON.stringify({
'@type': 'ListItem',
position: i + 3,
name: br.label,
item: br.href ? `https://tuono.dev${br.href}` : undefined,
}),
)
.join(',')}]
}
`}
</script>
</Head>
<Breadcrumbs
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
mb="md"
mt="md"
>
<IconBolt />
</Button>
{children}
</Breadcrumbs>
<Button
href="/documentation"
component={Link}
variant="subtle"
radius="xl"
p={5}
>
<IconBolt />
</Button>
{breadcrumbs.map((br) => (
<BreadcrumbElement href={br.href} label={br.label} key={br.label} />
))}
</Breadcrumbs>
</>
)
}
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Button } from '@mantine/core'
import { IconEdit } from '@tabler/icons-react'
import { useRouter } from 'tuono'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import {
Button,
Center,
@@ -15,7 +16,7 @@ export default function Hero(): JSX.Element {
return (
<Container size={1100} my={100}>
<Center>
<Title order={1}>The React/Rust full-stack framework</Title>
<Title order={1}>Superfast fullstack react framework</Title>
</Center>
<Center mt={50}>
<Text fz="18px">
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Text, type TextProps } from '@mantine/core'
export default function MdxBold(props: TextProps): JSX.Element {
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Code } from '@mantine/core'
import type { HTMLAttributes } from 'react'
@@ -1,4 +1,4 @@
import type { AnchorHTMLAttributes } from 'react'
import type { JSX, AnchorHTMLAttributes } from 'react'
import { Button } from '@mantine/core'
import { Link } from 'tuono'
import { IconExternalLink } from '@tabler/icons-react'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { CodeHighlight } from '@mantine/code-highlight'
import styles from './mdx-pre.module.css'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
import MdxLink from './mdx-link'
@@ -1,5 +1,5 @@
import type { JSX, HTMLAttributes } from 'react'
import { Blockquote, Space } from '@mantine/core'
import type { HTMLAttributes } from 'react'
export default function MdxQuote(
props: HTMLAttributes<HTMLQuoteElement>,
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import { Title, type TitleProps } from '@mantine/core'
export default function MdxTitle(props: TitleProps): JSX.Element {
@@ -0,0 +1,3 @@
import MetaTags from './meta-tags'
export default MetaTags
@@ -0,0 +1,46 @@
import type { ReactNode } from 'react'
import { Head } from 'tuono'
interface MetaTagsProps {
title: string
description?: string
type?: 'website' | 'article' | 'book' | 'video.movie' | 'music.song'
canonical: string
}
export default function MetaTags({
title,
description,
type = 'article',
canonical,
}: MetaTagsProps): ReactNode {
return (
<Head>
<title>{title}</title>
<link rel="canonical" href={canonical} />
<meta
name="description"
content={
description ??
'The technologies we love seamlessly working together to unleash the highest web performance ever met on React'
}
/>
<meta
name="keywords"
content="ReactJs, Typescript, Rust, JavaScript, Fullstack framework, Web development"
/>
<meta property="og:title" content={title} />
<meta property="og:type" content={type} />
<meta property="og:image" content="https://tuono.dev/og-cover.png" />
<meta property="og:url" content={canonical} />
<meta property="og:description" content={description} />
<meta property="og:site_name" content="Tuono" />
<meta property="og:locale" content="en_US" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@tuono_dev" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content="https://tuono.dev/og-cover.png" />
</Head>
)
}
@@ -1,8 +1,10 @@
import type { JSX } from 'react'
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
import { IconBrandGithub, IconBook, IconBrandX } from '@tabler/icons-react'
import ThemeBtn from '../theme-btn'
import { Link } from 'tuono'
import ThemeBtn from '../theme-btn'
export default function Actions(): JSX.Element {
return (
<Flex gap={8}>
@@ -1,5 +1,7 @@
import type { JSX } from 'react'
import { AppShell, Burger, Button, Flex } from '@mantine/core'
import { Link, useRouter } from 'tuono'
import Actions from './actions'
interface NavbarProps {
@@ -1,3 +1,4 @@
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'
@@ -1,5 +1,6 @@
import type { JSX, ReactNode } from 'react'
import { useState } from 'react'
import { NavLink, type NavLinkProps } from '@mantine/core'
import { useState, type ReactNode } from 'react'
import { Link, useRouter } from 'tuono'
import { IconChevronRight } from '@tabler/icons-react'
@@ -1,10 +1,16 @@
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="Overview" onClick={close} />
<SidebarLink
href="/documentation"
label="Getting started"
onClick={close}
/>
<SidebarLink
href="/documentation/installation"
label="Installation"
@@ -1,6 +1,7 @@
/*
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
*/
import type { JSX } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useRouter, Link } from 'tuono'
import { IconList } from '@tabler/icons-react'
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import {
ActionIcon,
useMantineColorScheme,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import {
ColorSchemeScript,
createTheme,
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import { AppShell, Container } from '@mantine/core'
import MdxProvider from '../../components/mdx-provider'
import EditPage from '../../components/edit-page'
@@ -1,15 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - CLI</title>
<link rel="canonical" href="https://tuono.dev/documentation/cli" />
</Head>
<MetaTags
title="Tuono - CLI"
canonical="https://tuono.dev/documentation/cli"
description="Tuono is the CLI that provides all the needed commands to handle the full-stack project."
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs>
<Element label="CLI" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
# CLI
@@ -1,15 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - Contributing</title>
<link rel="canonical" href="https://tuono.dev/documentation/contributing" />
</Head>
<MetaTags
title="Tuono - Contributing"
canonical="https://tuono.dev/documentation/contributing"
description="The project is massive - if you like it, do consider contributing!"
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs>
<Element label="✨ Contributing" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
# Contributing
@@ -1,9 +1,10 @@
import { Head } from 'tuono'
import MetaTags from '../../components/meta-tags'
<Head>
<title>Tuono - Documentation</title>
<link rel="canonical" href="https://tuono.dev/documentation" />
</Head>
<MetaTags
title="Tuono - Documentation"
canonical="https://tuono.dev/documentation"
description="Tuono is a full-stack framework for building React applications using Rust as the backend."
/>
import Breadcrumbs from '../../components/breadcrumbs'
@@ -11,39 +12,43 @@ import Breadcrumbs from '../../components/breadcrumbs'
# Tuono
Tuono is a full-stack framework for building React applications using Rust as the
backend.
Tuono is a full-stack web framework for building React applications using Rust as the
backend with a strong focus on usability and performance.
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
## Introduction
NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)
Tuono is a full-stack React framework with the server side written in Rust.
Because of this, Tuono is extremely fast and, the requests are handled by a multithreaded Rust server.
React is still React - it is just superpowered.
Rust is a hard language, then writing server side code is hard as well, right? (no again)
Tuono provides a collection of utilities to handle the server side code seamlessly with
the React code. Each server side route is managed in a separate file alongside the React route.
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
## Features
Some of its features include:
- 🟦 Full typescript support
- 🌐 File system based routing
- 🔥 Hot Module Reload
- 🍭 SCSS/CSS modules
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🧵 Multi-threaded backend
- ⌨️ MDX support
- ⚙️ Build optimizations
- Custom APIs\*
- Image optimization\*
- Server streamed content\*
- 🔥 Hot Module Reload
> \*development in progress
## Introduction
Tuono addresses the following two limits:
- Enable React server-side rendering projects without using JS runtimes (Node, Deno)
- Ease the web development using a typically hard language like Rust.
#### NodeJs/Deno/Bun are the only runtimes that allow a React app to be server-side rendered (SSR)? (no)
Tuono is the first full-stack React framework with the server side written in Rust (check the [ssr-rs](https://github.com/Valerioageno/ssr-rs) crate).
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
React is still React - it is just superpowered.
#### Rust is a hard language, then writing server side code is hard as well, right? (no again)
Tuono provides a collection of utilities to handle server-side code seamlessly with
the React code. Each server side route is managed in a separate file alongside the React route.
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
#### How is Tuono different from Next.js?
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
@@ -1,14 +1,14 @@
import { Head } from 'tuono'
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Head>
<title>Tuono - Installation</title>
<link rel="canonical" href="https://tuono.dev/documentation/installation" />
</Head>
import MetaTags from '../../components/meta-tags'
<Breadcrumbs>
<Element label="Installation" />
</Breadcrumbs>
<MetaTags
title="Tuono - Installation"
canonical="https://tuono.dev/documentation/installation"
description="Tuono is a development environment built in rust and typescript that outputs a website written in both the languages"
/>
<Breadcrumbs breadcrumbs={[{ label: 'Installation' }]} />
# Installation
@@ -1,15 +1,13 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Routing</title>
<link rel="canonical" href="https://tuono.dev/documentation/routing" />
</Head>
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing/intro"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Routing" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
# Routing index
@@ -1,16 +1,18 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Routing</title>
<link rel="canonical" href="https://tuono.dev/documentation/routing/intro" />
</Head>
<MetaTags
title="Tuono - Routing"
canonical="https://tuono.dev/documentation/routing"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Routing" href="/documentation/routing" />
<Element label="Project structure" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Routing', href: '/documentation/routing' },
{ label: 'Project structure' },
]}
/>
# Project structure
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - API fetching</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/api-fetching"
/>
</Head>
<MetaTags
title="Tuono - API fetching"
canonical="https://tuono.dev/documentation/tutorial/api-fetching"
description="Learn how to fetch remote APIs on the backend"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="API fetching" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'API fetching' },
]}
/>
# API fetching
@@ -56,10 +56,15 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
}
```
> 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 {
@@ -72,9 +77,9 @@ interface IndexProps {
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
}: TuonoProps<IndexProps>): JSX.Element | null {
if (!data?.results) {
return <></>
return null
}
return (
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Components</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/components"
/>
</Head>
<MetaTags
title="Tuono - Components"
canonical="https://tuono.dev/documentation/tutorial/components"
description="Learn how to manage the components in a Tuono codebase"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Components" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Components' },
]}
/>
# Components
@@ -25,6 +25,7 @@ Create the following file `src/components/PokemonLink.tsx` and fill the content
```tsx
// src/components/PokemonLink.tsx
import type { JSX } from 'react'
import { Link } from 'tuono'
interface Pokemon {
@@ -109,8 +110,9 @@ Then import the styles into the `PokemonLink` component as following:
```diff
// src/components/PokemonLink.tsx
import { Link } from "tuono";
import type { Pokemon } from "./../types/pokemon";
import type { JSX } from 'react'
import { Link } from "tuono"
import type { Pokemon } from "./../types/pokemon"
++ import styles from './PokemonLink.module.css'
export default function PokemonLink({
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Conclusion</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/conclusion"
/>
</Head>
<MetaTags
title="Tuono - Conclusion"
canonical="https://tuono.dev/documentation/tutorial/conclusion"
description="You just created a multi thread full stack application with Rust and React"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Conclusion" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Conclusion' },
]}
/>
# Conclusion
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Development setup</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/development-setup"
/>
</Head>
<MetaTags
title="Tuono - Development setup"
canonical="https://tuono.dev/documentation/tutorial/development-setup"
description="Learn how to setup a new Tuono project"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Development setup" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Development setup' },
]}
/>
# Development setup
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Dynamic routes</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/dynamic-routes"
/>
</Head>
<MetaTags
title="Tuono - Dynamic routes"
canonical="https://tuono.dev/documentation/tutorial/dynamic-routes"
description="Learn how to catch more endpoint with the same route loading dynamic data"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Dynamic routes" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Dynamic routes' },
]}
/>
# Dynamic routes
@@ -21,7 +21,7 @@ import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
The homepage is ready. We have the full list of Pokémon and they are all links. Great!
Now we want to make those links actually point to a real page. Lets create the \_\_ route.
Now we want to make those links actually point to a real page. Lets create the dynamic route.
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
@@ -63,6 +63,7 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
Then lets work on the frontend. Paste into the `[pokemon].tsx` file the following code:
```tsx
import type { JSX } from 'react'
import { TuonoProps } from 'tuono'
import PokemonView from '../../components/PokemonView'
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Error handling</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/error-handling"
/>
</Head>
<MetaTags
title="Tuono - Error handling"
canonical="https://tuono.dev/documentation/tutorial/error-handling"
description="Learn how to handle the server side error on your Tuono project"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Error handling" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Error handling' },
]}
/>
# Error handling
@@ -1,15 +1,14 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tuono - Tutorial</title>
<link rel="canonical" href="https://tuono.dev/documentation/tutorial" />
</Head>
<MetaTags
title="Tuono - Tutorial"
canonical="https://tuono.dev/documentation/tutorial"
description="This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the development - be sure to have installed the latest version"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" />
</Breadcrumbs>
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
# Tutorial
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Building for production</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/production"
/>
</Head>
<MetaTags
title="Tuono - Building for production"
canonical="https://tuono.dev/documentation/tutorial/production"
description="Learn how to build the production project and run the optimize version"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Production build" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Production build' },
]}
/>
# Production build
@@ -1,19 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - Redirections</title>
<link
rel="canonical"
href="https://tuono.dev/documentation/tutorial/redirections"
/>
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="Redirections" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'Redirections' },
]}
/>
# Redirections
@@ -25,10 +25,10 @@ First, let's create a new route by just creating a new file `/pokemons/GOAT.rs`
```rs
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
use tuono_lib::{Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> Response {
async fn redirect_to_goat(_req: Request) -> Response {
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
Response::Redirect("/pokemons/mewtwo".to_string())
}
@@ -1,16 +1,19 @@
import { Head } from 'tuono'
import MetaTags from '../../../components/meta-tags'
<Head>
<title>Tutorial - SEO and meta tags</title>
<link rel="canonical" href="https://tuono.dev/documentation/tutorial/seo" />
</Head>
<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>
<Element label="Tutorial" href="/documentation/tutorial" />
<Element label="SEO and meta tags" />
</Breadcrumbs>
<Breadcrumbs
breadcrumbs={[
{ label: 'Tutorial', href: '/documentation/tutorial' },
{ label: 'SEO and meta tags' },
]}
/>
# SEO and meta tags
@@ -23,6 +26,7 @@ To do so `tuono` also exposes the `<Head />` component useful exactly for handli
```diff
// src/routes/index.tsx
import type { JSX } from 'react'
import type { TuonoProps } from "tuono";
++ import { Head } from "tuono"
@@ -77,6 +81,7 @@ export default function IndexPage({
```diff
// src/routes/pokemons/[pokemon].tsx
import type { JSX } from 'react'
-- import { TuonoProps } from "tuono";
++ import { TuonoProps, Head } from "tuono";
import PokemonView from "../../components/PokemonView";
+8 -9
View File
@@ -1,17 +1,16 @@
import { Head } from 'tuono'
import type { JSX } from 'react'
import Hero from '../components/hero'
import MetaTags from '../components/meta-tags'
export default function IndexPage(): JSX.Element {
return (
<>
<Head>
<title>Tuono - The React/Rust full-stack framework</title>
<link rel="canonical" href="https://tuono.dev" />
<meta
name="description"
content="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
/>
</Head>
<MetaTags
title="Tuono - The React/Rust full-stack framework"
canonical="https://tuono.dev"
description="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
/>
<Hero />
</>
)
+2 -2
View File
@@ -1,4 +1,4 @@
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::reqwest::StatusCode;
use tuono_lib::{Request, Response};
use tuono_lib::axum::http::{header, HeaderMap};
use glob::glob;
@@ -36,7 +36,7 @@ fn clean_path(value: String) -> String {
}
#[tuono_lib::handler]
async fn generate_sitemap(_req: Request, _fetch: Client) -> Response {
async fn generate_sitemap(_req: Request) -> Response {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/xml".parse().unwrap());
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.11.1"
version = "0.12.1"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
+5 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.11.1"
version = "0.12.1"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
@@ -33,9 +33,12 @@ either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.11.1"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.12.1"}
# Match the same version used by axum
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tungstenite = "0.24.0"
http = "1.1.0"
pin-project = "1.1.7"
tower = "0.5.1"
+1
View File
@@ -5,6 +5,7 @@ mod payload;
mod request;
mod response;
mod server;
mod services;
mod ssr;
mod vite_reverse_proxy;
mod vite_websocket_proxy;
+2 -16
View File
@@ -3,9 +3,7 @@ use crate::{ssr::Js, Payload};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use axum::Json;
use colored::*;
use erased_serde::Serialize;
use tokio::time::Instant;
pub struct Props {
data: Box<dyn Serialize>,
@@ -74,25 +72,13 @@ impl Response {
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
match self {
Self::Props(Props { data, http_code }) => {
let start = Instant::now();
let payload = Payload::new(&req, data).client_payload().unwrap();
let response = match Js::render_to_string(Some(&payload)) {
match Js::render_to_string(Some(&payload)) {
Ok(html) => (*http_code, Html(html)),
Err(_) => (*http_code, Html("500 Internal server error".to_string())),
}
.into_response();
let duration = start.elapsed();
println!(
" GET {} {} in {}ms",
req.uri.path(),
http_code.as_str().green(),
duration.as_millis()
);
response
.into_response()
}
Self::Redirect(to) => Redirect::permanent(to).into_response(),
Self::Custom(response) => response.clone().into_response(),
+12 -4
View File
@@ -1,6 +1,6 @@
use crate::mode::{Mode, GLOBAL_MODE};
use crate::manifest::load_manifest;
use crate::mode::{Mode, GLOBAL_MODE};
use crate::services::logger::LoggerLayer;
use axum::routing::{get, Router};
use colored::Colorize;
use ssr_rs::Ssr;
@@ -42,9 +42,13 @@ impl Server {
let router = self
.router
.to_owned()
.layer(LoggerLayer::new())
.route("/vite-server/", get(vite_websocket_proxy))
.route("/vite-server/*path", get(vite_reverse_proxy))
.fallback_service(ServeDir::new(DEV_PUBLIC_DIR).fallback(get(catch_all)))
.fallback_service(
ServeDir::new(DEV_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
)
.with_state(fetch);
axum::serve(listener, router)
@@ -58,7 +62,11 @@ impl Server {
let router = self
.router
.to_owned()
.fallback_service(ServeDir::new(PROD_PUBLIC_DIR).fallback(get(catch_all)))
.layer(LoggerLayer::new())
.fallback_service(
ServeDir::new(PROD_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
)
.with_state(fetch);
axum::serve(listener, router)
+1
View File
@@ -0,0 +1 @@
pub mod logger;
+106
View File
@@ -0,0 +1,106 @@
use colored::Colorize;
use http::{method::Method, Request, Response};
use pin_project::pin_project;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::time::Instant;
use tower::{Layer, Service};
#[derive(Clone)]
pub struct LoggerLayer {}
impl LoggerLayer {
pub fn new() -> Self {
LoggerLayer {}
}
}
impl<S> Layer<S> for LoggerLayer {
type Service = Logger<S>;
fn layer(&self, inner: S) -> Self::Service {
Logger::new(inner)
}
}
#[derive(Clone)]
pub struct Logger<S> {
inner: S,
}
impl<S> Logger<S> {
pub fn new(inner: S) -> Self {
Logger { inner }
}
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Logger<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Default,
<S as Service<Request<ReqBody>>>::Error: Debug,
{
type Response = S::Response;
type Error = S::Error;
type Future = LoggerFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let method = req.method().clone();
let path = req.uri().path().to_string();
LoggerFuture {
future: self.inner.call(req),
method,
path,
start: Instant::now(),
}
}
}
#[pin_project]
pub struct LoggerFuture<F> {
#[pin]
future: F,
method: Method,
path: String,
start: Instant,
}
impl<F, B, E> Future for LoggerFuture<F>
where
F: Future<Output = Result<Response<B>, E>>,
B: Default,
E: Debug,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res: F::Output = match this.future.poll(cx) {
Poll::Ready(res) => res,
Poll::Pending => return Poll::Pending,
};
if this.path.starts_with("/__tuono/data") {
return Poll::Ready(res);
}
let status_code = res.as_ref().unwrap().status();
println!(
" {} {} {} in {}ms",
this.method,
this.path,
status_code.as_str().green(),
this.start.elapsed().as_millis()
);
Poll::Ready(res)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.11.1"
version = "0.12.1"
edition = "2021"
description = "The react/rust fullstack framework"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
+53 -16
View File
@@ -1,43 +1,80 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_macro_input, FnArg, ItemFn, Pat, Type};
fn create_struct_fn_arg(arg_name: Pat, arg_type: Type) -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(#arg_name): tuono_lib::axum::extract::State<#arg_type>
})
.unwrap()
}
fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let fn_name = item.clone().sig.ident;
let fn_name = &item.sig.ident;
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
// Fn Arguments minus the first which always is the request
for (i, arg) in item.sig.inputs.iter().enumerate() {
if i == 0 {
axum_arguments.insert(i, params_argument());
continue;
}
if let FnArg::Typed(pat_type) = arg {
let index = i - 1;
let argument_name = *pat_type.pat.clone();
let argument_type = *pat_type.ty.clone();
argument_names.insert(index, argument_name.clone());
axum_arguments.insert(index, create_struct_fn_arg(argument_name, argument_type))
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
quote! {
use tuono_lib::axum::response::IntoResponse;
use std::collections::HashMap;
use tuono_lib::axum::extract::{State, Path};
#item
pub async fn route(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse {
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.render_to_string(req)
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
}
pub async fn api(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse{
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.json()
#fn_name(req.clone(), #argument_names).await.json()
}
}
.into()
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
import { MDXProvider } from '@mdx-js/react'
interface RootRouteProps {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
interface RootRouteProps {
children: ReactNode
+1 -2
View File
@@ -1,5 +1,4 @@
use serde::Serialize;
use tuono_lib::reqwest;
use tuono_lib::{Props, Request, Response};
#[derive(Serialize)]
@@ -8,7 +7,7 @@ struct MyResponse<'a> {
}
#[tuono_lib::handler]
async fn get_server_side_props(_req: Request, _fetch: reqwest::Client) -> Response {
async fn get_server_side_props(_req: Request) -> Response {
Response::Props(Props::new(MyResponse {
subtitle: "The react / rust fullstack framework",
}))
+1
View File
@@ -1,3 +1,4 @@
import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'
interface IndexProps {
@@ -1,4 +1,6 @@
import type { JSX } from 'react'
import { Link } from 'tuono'
import styles from './PokemonLink.module.css'
interface Pokemon {
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react'
import type { ReactNode, JSX } from 'react'
interface RootRouteProps {
children: ReactNode
+4 -2
View File
@@ -1,5 +1,7 @@
// src/routes/index.tsx
import type { JSX } from 'react'
import { Head, type TuonoProps } from 'tuono'
import PokemonLink from '../components/PokemonLink'
interface Pokemon {
@@ -12,9 +14,9 @@ interface IndexProps {
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
}: TuonoProps<IndexProps>): JSX.Element | null {
if (!data?.results) {
return <></>
return null
}
return (
@@ -1,7 +1,7 @@
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
use tuono_lib::{Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> Response {
async fn redirect_to_goat(_req: Request) -> Response {
Response::Redirect("/pokemons/mewtwo".to_string())
}
@@ -1,22 +1,24 @@
import type { JSX } from 'react'
import { Head, type TuonoProps } from 'tuono'
import PokemonView from '../../components/PokemonView'
interface Pokemon {
name: string
id: string
weight: number
height: number
name: string
id: string
weight: number
height: number
}
export default function PokemonPage({
data,
data,
}: TuonoProps<Pokemon>): JSX.Element {
return (
<>
<Head>
<title>{`Pokemon: ${data?.name}`}</title>
</Head>
<PokemonView pokemon={data} />
</>
)
return (
<>
<Head>
<title>{`Pokemon: ${data?.name}`}</title>
</Head>
<PokemonView pokemon={data} />
</>
)
}
+15 -16
View File
@@ -1,22 +1,21 @@
{
"name": "workspace",
"packageManager": "pnpm@9.12.3+sha256.24235772cc4ac82a62627cd47f834c72667a2ce87799a846ec4e8e555e2d4b8b",
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
"scripts": {
"dev": "turbo watch dev",
"build": "turbo build --filter tuono",
"lint": "turbo lint",
"format": "turbo format",
"format:check": "turbo format:check",
"types": "turbo types --filter tuono",
"test": "turbo test",
"test:watch": "turbo test:watch"
"dev": "turbo dev --filter=./packages/*",
"build": "turbo build --filter=./packages/*",
"lint": "turbo lint --filter=./packages/*",
"format": "turbo format --filter=./packages/*",
"format:check": "turbo format:check --filter=./packages/*",
"types": "turbo types --filter=./packages/*",
"test": "turbo test --filter=./packages/*",
"test:watch": "turbo test:watch --filter=./packages/*",
"docs:lint": "turbo lint --filter=documentation",
"docs:format": "turbo format --filter=documentation",
"docs:format:check": "turbo format:check --filter=documentation",
"docs:types": "turbo types --filter=documentation",
"check-all": "turbo build lint format:check types"
},
"workspaces": [
"tuono",
"tuono-lazy-fn-vite-plugin",
"tuono-fs-router-vite-plugin",
"tuono-router"
],
"author": "Valerio Ageno",
"license": "MIT",
"devDependencies": {
@@ -38,6 +37,6 @@
"vitest": "^2.0.0"
},
"dependencies": {
"turbo": "^2.0.12"
"turbo": "^2.2.3"
}
}
@@ -0,0 +1,2 @@
dist
pnpm-lock.yaml
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.11.1",
"version": "0.12.1",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.11.1",
"version": "0.12.1",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-router",
"version": "0.11.1",
"version": "0.12.1",
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+1 -1
View File
@@ -17,7 +17,7 @@ interface TuonoLinkProps {
export default function Link(
componentProps: AnchorHTMLAttributes<HTMLAnchorElement> & TuonoLinkProps,
): JSX.Element {
): React.JSX.Element {
const { preload = true, scroll = true, ...props } = componentProps
const router = useRouter()
const route = useRoute(props.href)
+1 -1
View File
@@ -9,7 +9,7 @@ interface MatchesProps {
serverSideProps: any
}
export function Matches({ serverSideProps }: MatchesProps): JSX.Element {
export function Matches({ serverSideProps }: MatchesProps): React.JSX.Element {
const location = useRouterStore((st) => st.location)
const route = useRoute(location.pathname)
+1 -1
View File
@@ -3,7 +3,7 @@ import { RouteMatch } from './RouteMatch'
import Link from './Link'
import { useInternalRouter } from '../hooks/useInternalRouter'
export default function NotFound(): JSX.Element {
export default function NotFound(): React.JSX.Element {
const router = useInternalRouter()
const custom404Route = router.routesById['/404']
@@ -16,7 +16,7 @@ interface MatchProps {
export const RouteMatch = ({
route,
serverSideProps,
}: MatchProps): JSX.Element => {
}: MatchProps): React.JSX.Element => {
const { data, isLoading } = useServerSideProps(route, serverSideProps)
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
@@ -55,13 +55,13 @@ const TraverseRootComponents = React.memo(
isLoading,
index = 0,
children,
}: TraverseRootComponentsProps): JSX.Element => {
}: TraverseRootComponentsProps): React.JSX.Element => {
if (routes.length > index) {
const Parent = React.useMemo(
() =>
routes[index]?.component as unknown as (
props: ParentProps,
) => JSX.Element,
) => React.JSX.Element,
[],
)
@@ -1,9 +1,10 @@
import { getRouterContext } from './RouterContext'
import { Matches } from './Matches'
import React from 'react'
import type { ReactNode, JSX } from 'react'
import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
import React, { type ReactNode } from 'react'
import { initRouterStore } from '../hooks/useRouterStore'
import type { ServerProps } from '../types'
import { getRouterContext } from './RouterContext'
import { Matches } from './Matches'
type Router = any
+15 -3
View File
@@ -1,4 +1,5 @@
import * as React from 'react'
import type { ReactElement, ComponentType } from 'react'
import type { RouteComponent } from './types'
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
@@ -12,7 +13,7 @@ type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
* It can be wrapped within a React.Suspense component in order to handle its loading state.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const dynamic = (importFn: ImportFn): JSX.Element => {
export const dynamic = (importFn: ImportFn): React.JSX.Element => {
/**
*
* This function is just a placeholder. The real work is done by the bundler.
@@ -40,7 +41,18 @@ export const dynamic = (importFn: ImportFn): JSX.Element => {
}
export const lazyLoadComponent = (factory: ImportFn): RouteComponent => {
const Component = React.lazy(factory) as unknown as RouteComponent
Component.preload = factory
let LoadedComponent: ComponentType<any> | undefined
const LazyComponent = React.lazy(factory) as unknown as RouteComponent
const loadComponent = (): Promise<void> =>
factory().then((module) => {
LoadedComponent = module.default
})
const Component = (props: any): ReactElement =>
React.createElement(LoadedComponent || LazyComponent, props)
Component.preload = loadComponent
return Component
}
+2
View File
@@ -1,3 +1,5 @@
import type { JSX } from 'react'
import { trimPath, trimPathRight } from './utils'
import type { Route } from './route'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.11.1",
"version": "0.12.1",
"description": "The react/rust fullstack framework",
"scripts": {
"dev": "vite build --watch",
+3 -3
View File
@@ -1,4 +1,4 @@
packages:
- "packages/*"
- "!**/examples/**"
- "!**/apps/**"
- 'packages/*'
- 'apps/*'
- '!**/examples/**'