mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c394898fcd | |||
| f22249f3e9 | |||
| 120cec52e9 | |||
| 535cc6b3d9 | |||
| 18a060e2bf | |||
| 7b8165cee6 | |||
| 6331eb64e6 | |||
| 3c38b551f0 | |||
| 8e761347f2 | |||
| e2d44e0e6a | |||
| 607da92c71 | |||
| ae0427604e | |||
| 2aa981c581 | |||
| 5d8ec82502 | |||
| fc9bb52c41 | |||
| 3224e4138f | |||
| 7ea28c4fd3 | |||
| acb5567f6a | |||
| cc877c7c11 | |||
| 87bdddf7cf | |||
| d939b64d64 | |||
| 3f64f71947 | |||
| aaae7db24d | |||
| 48c3934b6e |
@@ -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,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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,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. Let’s create the \_\_ route.
|
||||
Now we want to make those links actually point to a real page. Let’s create the dynamic route.
|
||||
|
||||
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
|
||||
|
||||
@@ -63,6 +63,7 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||
Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code:
|
||||
|
||||
```tsx
|
||||
import type { JSX } from 'react'
|
||||
import { TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ mod payload;
|
||||
mod request;
|
||||
mod response;
|
||||
mod server;
|
||||
mod services;
|
||||
mod ssr;
|
||||
mod vite_reverse_proxy;
|
||||
mod vite_websocket_proxy;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod logger;
|
||||
@@ -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,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"]
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
|
||||
interface RootRouteProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
|
||||
@@ -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,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,4 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
|
||||
@@ -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
@@ -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,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,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,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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { trimPath, trimPathRight } from './utils'
|
||||
import type { Route } from './route'
|
||||
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
- "packages/*"
|
||||
- "!**/examples/**"
|
||||
- "!**/apps/**"
|
||||
- 'packages/*'
|
||||
- 'apps/*'
|
||||
- '!**/examples/**'
|
||||
|
||||
Reference in New Issue
Block a user