mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 13:52:46 -07:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cbcd8ad70 | |||
| 9f40a55907 | |||
| 51e01ec142 | |||
| 63fc2b9855 | |||
| edd8a02423 | |||
| a0a81b514c | |||
| 92820804f8 | |||
| e7ea7ed1c4 | |||
| 7471c90f49 | |||
| f545a9c469 | |||
| 30073c226d | |||
| 1b1e91b6c0 | |||
| 71d7ccf27d | |||
| 3ca24ec87f | |||
| b2940d9fa8 | |||
| a7b7f0eac2 | |||
| 4859916edd | |||
| ad3e19e965 | |||
| 03dead1083 | |||
| 341fba6ca9 | |||
| 58c4890122 | |||
| af42e5ef21 | |||
| 5abff0384c | |||
| cc915560b8 | |||
| 9835a4c0d1 | |||
| 90782ae710 | |||
| 75c790dd6e | |||
| 8604858a61 | |||
| d1b6162d3a | |||
| a445644b47 | |||
| ebb4434506 | |||
| 36a53662a0 | |||
| 569d95dc9c | |||
| bc4c4e03f0 | |||
| 170c5ea098 | |||
| 3fd5f166e2 | |||
| a2285a78a1 | |||
| 1ae7f72de3 | |||
| 19782a2dbe | |||
| b755e658f5 | |||
| cc0279b964 | |||
| 099da549b5 | |||
| 92a76032eb | |||
| cd17eebf96 | |||
| 1726312d34 | |||
| 444d1479b0 | |||
| 2949ea4dfa | |||
| 8ab292b2f9 | |||
| 9d8bfd9826 | |||
| 8a6685a6d0 | |||
| 78990d3c08 | |||
| 227662489b | |||
| 5ce410f233 | |||
| 67d4e1bcac | |||
| 3e6aa540fa | |||
| b412371287 | |||
| f62fc0b2b9 | |||
| 35d8154649 | |||
| 6e794e9500 | |||
| e4d4a75b88 | |||
| bc7ffc2246 |
@@ -0,0 +1,23 @@
|
|||||||
|
## 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,20 @@
|
|||||||
|
---
|
||||||
|
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,13 @@
|
|||||||
|
## Context & Description
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thank you for your Pull Request.
|
||||||
|
|
||||||
|
Explain the context and why you're making that change. What is the problem
|
||||||
|
you're trying to solve? If a new feature is being added, describe the intended
|
||||||
|
use case that feature fulfills.
|
||||||
|
|
||||||
|
Bug fixes and new features should include tests.
|
||||||
|
|
||||||
|
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
|
||||||
|
-->
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
name: Documentation Website CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '.github/**'
|
||||||
|
- 'apps/documentation/**'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/**'
|
||||||
|
- 'apps/documentation/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||||
|
timeout-minutes: 15
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./apps/documentation
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Setup rust
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
|
||||||
|
- name: Install tuono
|
||||||
|
run: cargo install tuono@0.8.3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm i -g pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --ignore-workspace
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: tuono build --static
|
||||||
|
|
||||||
|
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
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm i -g pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --ignore-workspace
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: pnpm format:check
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Types
|
||||||
|
run: pnpm types
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Deploy documentation website on AWS S3
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '.github/**'
|
||||||
|
- 'apps/documentation/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-documentation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./apps/documentation
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Setup rust
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
|
|
||||||
|
- name: Install tuono
|
||||||
|
run: cargo install tuono@0.8.3
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
run: npm i -g pnpm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --ignore-workspace
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: tuono build --static
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
uses: reggionick/s3-deploy@v4
|
||||||
|
with:
|
||||||
|
folder: apps/documentation/out/static
|
||||||
|
bucket: tuono-documentation
|
||||||
|
bucket-region: eu-west-3
|
||||||
|
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||||
|
invalidation: /
|
||||||
|
delete-removed: true
|
||||||
|
no-cache: true
|
||||||
|
private: true
|
||||||
|
files-to-include: '{.*/**,**}'
|
||||||
+2
-1
@@ -6,7 +6,8 @@ members = [
|
|||||||
"crates/tuono_lib_macros",
|
"crates/tuono_lib_macros",
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"examples/playground",
|
"apps/documentation",
|
||||||
|
"examples/mdx",
|
||||||
"examples/tuono",
|
"examples/tuono",
|
||||||
"examples/tutorial"
|
"examples/tutorial"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
<p align="center">
|
[Documentation](https://tuono.dev) | [Tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) |
|
||||||
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
|
[✨Contributing](https://tuono.dev/documentation/contributing)
|
||||||
</p>
|
|
||||||
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
|
|
||||||
<p align="center">
|
|
||||||
⚠️ This project is under heavy development. API might drastically change ⚠️
|
|
||||||
</p>
|
|
||||||
<div align="center">
|
|
||||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg" />
|
|
||||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br>
|
# Tuono
|
||||||
<br>
|
|
||||||
|
|
||||||
|
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
**Tuono** is the react/rust fullstack framework.
|
||||||
|
|
||||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||||
Why Tuono? Just a badass name.
|
Why Tuono? Just a badass name.
|
||||||
@@ -41,6 +38,7 @@ by Tuono based on the files defined within the `./src/routes` directory.
|
|||||||
- 🍭 CSS modules
|
- 🍭 CSS modules
|
||||||
- 🧬 Server Side Rendering
|
- 🧬 Server Side Rendering
|
||||||
- 🧵 Multi thread backend
|
- 🧵 Multi thread backend
|
||||||
|
- ⌨️ MDX support
|
||||||
- ⚙️ Build optimizations
|
- ⚙️ Build optimizations
|
||||||
- Custom APIs*
|
- Custom APIs*
|
||||||
- Image optimization*
|
- Image optimization*
|
||||||
@@ -53,19 +51,21 @@ by Tuono based on the files defined within the `./src/routes` directory.
|
|||||||
As already mentioned above I strongly suggest you to take a look at the
|
As already mentioned above I strongly suggest you to take a look at the
|
||||||
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
||||||
|
|
||||||
Tuono is basically a CLI that provides all the commands to handle the fullstack project.
|
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
|
||||||
To download it you need [cargo](https://doc.rust-lang.org/cargo/) which is the [rust](https://www.rust-lang.org/)
|
To download it is required [cargo](https://doc.rust-lang.org/cargo/) which is the [rust](https://www.rust-lang.org/)
|
||||||
package manager.
|
package manager.
|
||||||
|
|
||||||
To download and install it you just need to run `cargo install tuono`.
|
Then run `cargo install tuono`.
|
||||||
|
|
||||||
To create a new project run `tuono new [NAME]` (optionally you can pass the `--template` flag - check the
|
To list all the available commands run `tuono -h`
|
||||||
|
|
||||||
|
To create a new project run `tuono new [NAME]` (optionally you can pass the `--template` (or `-t`) flag - check the
|
||||||
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
||||||
|
|
||||||
Then to run the local development environment run inside the project folder `tuono dev`
|
Then to run the local development environment run inside the project folder `tuono dev`
|
||||||
|
|
||||||
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets
|
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets
|
||||||
and to set the server project to the `production` mode.
|
and to set the server project in the `production` mode.
|
||||||
|
|
||||||
Now to execute it just run `cargo run --release`.
|
Now to execute it just run `cargo run --release`.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# Stadalone apps folder
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"reportUnusedDisableDirectives": true,
|
||||||
|
"ignorePatterns": ["**/build", "**/dist"],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"plugins": ["@typescript-eslint", "import"],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/stylistic",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2020": true,
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"tsconfigRootDir": ".",
|
||||||
|
"project": true,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/parsers": {
|
||||||
|
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||||
|
},
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": true,
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"version": "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/array-type": "error",
|
||||||
|
"@typescript-eslint/ban-types": "error",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "error",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "error",
|
||||||
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
"error",
|
||||||
|
{ "prefer": "type-imports" },
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||||
|
"@typescript-eslint/method-signature-style": ["error", "property"],
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "typeParameter",
|
||||||
|
"format": ["PascalCase"],
|
||||||
|
"leadingUnderscore": "forbid",
|
||||||
|
"trailingUnderscore": "forbid",
|
||||||
|
"custom": {
|
||||||
|
"regex": "^(T|T[A-Z][A-Za-z]+)$",
|
||||||
|
"match": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-empty-function": "error",
|
||||||
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "error",
|
||||||
|
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": "error",
|
||||||
|
"@typescript-eslint/no-inferrable-types": [
|
||||||
|
"error",
|
||||||
|
{ "ignoreParameters": true },
|
||||||
|
],
|
||||||
|
"import/default": "error",
|
||||||
|
"import/export": "error",
|
||||||
|
"import/namespace": "error",
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-cycle": "error",
|
||||||
|
"import/no-duplicates": "off",
|
||||||
|
"import/no-named-as-default-member": "error",
|
||||||
|
"import/no-unused-modules": "error",
|
||||||
|
"import/order": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal",
|
||||||
|
"parent",
|
||||||
|
"sibling",
|
||||||
|
"index",
|
||||||
|
"object",
|
||||||
|
"type",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-prototype-builtins": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-shadow": "error",
|
||||||
|
"no-undef": "off",
|
||||||
|
"sort-imports": "off",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.tuono
|
||||||
|
out
|
||||||
|
target
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "tuono"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tuono"
|
||||||
|
path = ".tuono/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tuono_lib = "0.9.1"
|
||||||
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Tuono starter
|
||||||
|
|
||||||
|
This is the starter tuono project. To download it run in your terminal:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tuono new [NAME]
|
||||||
|
```
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "tuono",
|
||||||
|
"description": "The react/rust fullstack framework",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
||||||
|
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
||||||
|
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
||||||
|
"types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/code-highlight": "^7.11.2",
|
||||||
|
"@mantine/core": "^7.11.2",
|
||||||
|
"@mantine/hooks": "^7.11.2",
|
||||||
|
"@mdx-js/react": "^3.0.1",
|
||||||
|
"@tabler/icons-react": "^3.11.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"tuono": "0.9.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/mdx": "^2.0.13",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||||
|
"@typescript-eslint/parser": "^7.7.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"postcss": "^8.4.39",
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"prettier": "^3.2.4",
|
||||||
|
"typescript": "^5.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-preset-mantine': {},
|
||||||
|
'postcss-simple-vars': {
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
import { Breadcrumbs, Button } from '@mantine/core'
|
||||||
|
import { Link } from 'tuono'
|
||||||
|
|
||||||
|
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export default function TuonoBreadcrumbs({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}): 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}
|
||||||
|
>
|
||||||
|
<IconBolt />
|
||||||
|
</Button>
|
||||||
|
{children}
|
||||||
|
</Breadcrumbs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BreadcrumbElementProps {
|
||||||
|
href?: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BreadcrumbElement({
|
||||||
|
href,
|
||||||
|
label,
|
||||||
|
}: BreadcrumbElementProps): JSX.Element {
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<Button component={Link} href={href} variant="subtle" radius="xl" px={10}>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button component="span" variant="light" radius="xl">
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Breadcrumbs, { BreadcrumbElement } from './breadcrumbs'
|
||||||
|
|
||||||
|
export default Breadcrumbs
|
||||||
|
|
||||||
|
export { BreadcrumbElement as Element }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Button } from '@mantine/core'
|
||||||
|
import { IconEdit } from '@tabler/icons-react'
|
||||||
|
import { useRouter } from 'tuono'
|
||||||
|
|
||||||
|
const GITHUB_URL =
|
||||||
|
'https://github.com/Valerioageno/tuono/tree/main/apps/documentation/src/routes'
|
||||||
|
|
||||||
|
export default function EditPage(): JSX.Element {
|
||||||
|
const { pathname } = useRouter()
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
p={0}
|
||||||
|
mt={60}
|
||||||
|
component="a"
|
||||||
|
variant="transparent"
|
||||||
|
leftSection={<IconEdit />}
|
||||||
|
target="_blank"
|
||||||
|
href={GITHUB_URL.concat(pathname).concat('.mdx')}
|
||||||
|
>
|
||||||
|
Edit page
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import EditPage from './edit-page'
|
||||||
|
|
||||||
|
export default EditPage
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Container,
|
||||||
|
CopyButton,
|
||||||
|
Group,
|
||||||
|
rem,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { IconCopy, IconCheck } from '@tabler/icons-react'
|
||||||
|
import { Link } from 'tuono'
|
||||||
|
|
||||||
|
export default function Hero(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Container size={1100} mt={200}>
|
||||||
|
<Center>
|
||||||
|
<Title order={1}>The react/rust fullstack framework</Title>
|
||||||
|
</Center>
|
||||||
|
<Center mt={50}>
|
||||||
|
<Text fz="18px">
|
||||||
|
The technologies we love seamessly working together to unleash the
|
||||||
|
<strong> highest web performance</strong> ever met on react
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
<Center mt={50}>
|
||||||
|
<Group>
|
||||||
|
<Button component={Link} href="/documentation" size="lg">
|
||||||
|
Get Started
|
||||||
|
</Button>
|
||||||
|
<CopyButton value="cargo install tuono">
|
||||||
|
{({ copied, copy }) => (
|
||||||
|
<Button
|
||||||
|
onClick={copy}
|
||||||
|
size="lg"
|
||||||
|
style={{ border: 'solid 1px var(--mantine-color-violet-1)' }}
|
||||||
|
color="gray"
|
||||||
|
leftSection="cargo install tuono"
|
||||||
|
rightSection={
|
||||||
|
copied ? (
|
||||||
|
<IconCheck style={{ width: rem(20) }} />
|
||||||
|
) : (
|
||||||
|
<IconCopy style={{ width: rem(20) }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CopyButton>
|
||||||
|
</Group>
|
||||||
|
</Center>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Hero from './hero'
|
||||||
|
|
||||||
|
export default Hero
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxProvider from './mdx-provider'
|
||||||
|
|
||||||
|
export default MdxProvider
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Text, type TextProps } from '@mantine/core'
|
||||||
|
|
||||||
|
export default function MdxBold(props: TextProps): JSX.Element {
|
||||||
|
return <Text fw={700} {...props} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxCode from './mdx-code'
|
||||||
|
|
||||||
|
export default MdxCode
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Code } from '@mantine/core'
|
||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export default function MdxCode(
|
||||||
|
props: HTMLAttributes<HTMLPreElement>,
|
||||||
|
): JSX.Element {
|
||||||
|
return <Code {...props} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Title } from '@mantine/core'
|
||||||
|
import { useRef, type HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export default function MdxH2(
|
||||||
|
props: HTMLAttributes<HTMLHeadingElement>,
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Title
|
||||||
|
{...props}
|
||||||
|
mt={20}
|
||||||
|
order={2}
|
||||||
|
id={String(props.children ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(' ', '-')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxLink from './mdx-link'
|
||||||
|
|
||||||
|
export default MdxLink
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { AnchorHTMLAttributes } from 'react'
|
||||||
|
import { Button } from '@mantine/core'
|
||||||
|
import { Link } from 'tuono'
|
||||||
|
import { IconExternalLink } from '@tabler/icons-react'
|
||||||
|
|
||||||
|
export default function MdxLink(
|
||||||
|
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
|
): JSX.Element {
|
||||||
|
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
{...props}
|
||||||
|
target="_blank"
|
||||||
|
rightSection={
|
||||||
|
<IconExternalLink size="16px" style={{ marginLeft: -5 }} />
|
||||||
|
}
|
||||||
|
variant="transparent"
|
||||||
|
style={{ height: '20px' }}
|
||||||
|
mt={-2}
|
||||||
|
p={0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
{...props}
|
||||||
|
target="_blank"
|
||||||
|
variant="transparent"
|
||||||
|
style={{ height: '20px' }}
|
||||||
|
mt={-2}
|
||||||
|
p={0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxPre from './mdx-pre'
|
||||||
|
|
||||||
|
export default MdxPre
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.pre {
|
||||||
|
code {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { CodeHighlight } from '@mantine/code-highlight'
|
||||||
|
import styles from './mdx-pre.module.css'
|
||||||
|
|
||||||
|
interface PreProps {
|
||||||
|
children: any
|
||||||
|
}
|
||||||
|
export default function MdxPre({ children }: PreProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<CodeHighlight
|
||||||
|
className={styles.pre}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
code={children.props.children || ''}
|
||||||
|
language={children.props.className?.replace('language-', '')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { MDXProvider } from '@mdx-js/react'
|
||||||
|
|
||||||
|
import MdxLink from './mdx-link'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import MdxPre from './mdx-pre'
|
||||||
|
import MdxQuote from './mdx-quote'
|
||||||
|
import MdxCode from './mdx-code'
|
||||||
|
import MdxTitle from './mdx-title'
|
||||||
|
import MdxBold from './mdx-bold/mdx-bold'
|
||||||
|
import MdxH2 from './mdx-h2/mdx-h2'
|
||||||
|
|
||||||
|
interface MdxProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MdxProvider({
|
||||||
|
children,
|
||||||
|
}: MdxProviderProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<MDXProvider
|
||||||
|
components={{
|
||||||
|
a: MdxLink,
|
||||||
|
// @ts-expect-error: useless finding the correct props types
|
||||||
|
pre: MdxPre,
|
||||||
|
blockquote: MdxQuote,
|
||||||
|
code: MdxCode,
|
||||||
|
h1: MdxTitle,
|
||||||
|
h2: MdxH2,
|
||||||
|
strong: MdxBold,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</MDXProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxQuote from './mdx-quote'
|
||||||
|
|
||||||
|
export default MdxQuote
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Blockquote, Space } from '@mantine/core'
|
||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export default function MdxQuote(
|
||||||
|
props: HTMLAttributes<HTMLQuoteElement>,
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Blockquote
|
||||||
|
color="violet"
|
||||||
|
py={1}
|
||||||
|
px={20}
|
||||||
|
mt={30}
|
||||||
|
iconSize={30}
|
||||||
|
{...props}
|
||||||
|
style={{ borderRadius: 8 }}
|
||||||
|
/>
|
||||||
|
<Space h="md" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import MdxTitle from './mdx-title'
|
||||||
|
|
||||||
|
export default MdxTitle
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Title } from '@mantine/core'
|
||||||
|
import type { HTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export default function MdxTitle(
|
||||||
|
props: HTMLAttributes<HTMLHeadingElement>,
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Title
|
||||||
|
{...props}
|
||||||
|
order={1}
|
||||||
|
id={String(props.children ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(' ', '-')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||||
|
import { IconBrandGithub, IconBook } from '@tabler/icons-react'
|
||||||
|
import ThemeBtn from '../theme-btn'
|
||||||
|
import { Link } from 'tuono'
|
||||||
|
|
||||||
|
export default function Actions(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Flex gap={8}>
|
||||||
|
<Button
|
||||||
|
href="/documentation"
|
||||||
|
component={Link}
|
||||||
|
size="compact-lg"
|
||||||
|
rightSection={<IconBook />}
|
||||||
|
autoContrast
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
<Group gap={8} visibleFrom="sm">
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Check the project on github"
|
||||||
|
href="https://github.com/Valerioageno/tuono"
|
||||||
|
target="_blank"
|
||||||
|
component="a"
|
||||||
|
>
|
||||||
|
<IconBrandGithub />
|
||||||
|
</ActionIcon>
|
||||||
|
<ThemeBtn />
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Navbar from './navbar'
|
||||||
|
|
||||||
|
export default Navbar
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
||||||
|
import { Link, useRouter } from 'tuono'
|
||||||
|
import Actions from './actions'
|
||||||
|
|
||||||
|
interface NavbarProps {
|
||||||
|
opened: boolean
|
||||||
|
toggle: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||||
|
const { pathname } = useRouter()
|
||||||
|
return (
|
||||||
|
<AppShell.Header p="sm">
|
||||||
|
<Flex justify="space-between">
|
||||||
|
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||||
|
Tuono
|
||||||
|
</Button>
|
||||||
|
<Flex align="center" gap={8}>
|
||||||
|
<Actions />
|
||||||
|
{pathname.startsWith('/documentation') && (
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={toggle}
|
||||||
|
hiddenFrom="sm"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</AppShell.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Sidebar from './sidebar'
|
||||||
|
|
||||||
|
export default Sidebar
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { NavLink, type NavLinkProps } from '@mantine/core'
|
||||||
|
import { useState, type ReactNode } from 'react'
|
||||||
|
import { Link, useRouter } from 'tuono'
|
||||||
|
import { IconChevronRight } from '@tabler/icons-react'
|
||||||
|
|
||||||
|
import styles from './sidebar-link.module.css'
|
||||||
|
|
||||||
|
interface SidebarLinkProps {
|
||||||
|
label: string
|
||||||
|
href: string
|
||||||
|
onClick?: () => void
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SidebarLink(
|
||||||
|
props: SidebarLinkProps & NavLinkProps,
|
||||||
|
): JSX.Element {
|
||||||
|
const { pathname } = useRouter()
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
|
||||||
|
|
||||||
|
const internalProps = {
|
||||||
|
active: pathname === props.href,
|
||||||
|
className: styles.link,
|
||||||
|
rightSection: props.children && (
|
||||||
|
<IconChevronRight
|
||||||
|
size="1.2rem"
|
||||||
|
stroke={1.5}
|
||||||
|
className="mantine-rotate-rtl"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsOpen((state) => !state)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
opened: isOpen,
|
||||||
|
autoContrast: true,
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.href.startsWith('#')) {
|
||||||
|
return <NavLink component="button" {...internalProps} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NavLink component={Link} {...internalProps} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { AppShell } from '@mantine/core'
|
||||||
|
import SidebarLink from './sidebar-link'
|
||||||
|
|
||||||
|
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<AppShell.Navbar p="md">
|
||||||
|
<h3>Tutorial</h3>
|
||||||
|
<SidebarLink
|
||||||
|
href="/documentation/tutorial/intro"
|
||||||
|
label="Intro"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
<h3>Documentation</h3>
|
||||||
|
<SidebarLink href="/documentation" label="Overview" onClick={close} />
|
||||||
|
<SidebarLink
|
||||||
|
href="/documentation/installation"
|
||||||
|
label="Installation"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
<SidebarLink
|
||||||
|
href="/documentation/getting-started"
|
||||||
|
label="Getting started"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
<SidebarLink
|
||||||
|
label="Routing"
|
||||||
|
href="/documentation/routing"
|
||||||
|
defaultOpened
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<SidebarLink
|
||||||
|
href="/documentation/routing/intro"
|
||||||
|
label="Project structure"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
</SidebarLink>
|
||||||
|
<SidebarLink
|
||||||
|
label="Contributing"
|
||||||
|
href="/documentation/contributing"
|
||||||
|
onClick={close}
|
||||||
|
leftSection="✨"
|
||||||
|
/>
|
||||||
|
</AppShell.Navbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import ThemeBtn from './theme-btn'
|
||||||
|
|
||||||
|
export default ThemeBtn
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
.icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
@mixin dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
@mixin light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useComputedColorScheme,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { IconSun, IconMoon } from '@tabler/icons-react'
|
||||||
|
import cx from 'clsx'
|
||||||
|
|
||||||
|
import classes from './theme-btn.module.css'
|
||||||
|
|
||||||
|
export default function ThemeBtn(): JSX.Element {
|
||||||
|
const { setColorScheme } = useMantineColorScheme()
|
||||||
|
const computedColorScheme = useComputedColorScheme('light', {
|
||||||
|
getInitialValueInEffect: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() =>
|
||||||
|
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
aria-label="Toggle color scheme"
|
||||||
|
>
|
||||||
|
<IconSun className={cx(classes.icon, classes.light)} stroke={1.5} />
|
||||||
|
<IconMoon className={cx(classes.icon, classes.dark)} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
)
|
||||||
|
}
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
// declaration.d.ts
|
||||||
|
declare module '*.css'
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import {
|
||||||
|
ColorSchemeScript,
|
||||||
|
createTheme,
|
||||||
|
MantineProvider,
|
||||||
|
AppShell,
|
||||||
|
} from '@mantine/core'
|
||||||
|
import { useDisclosure } from '@mantine/hooks'
|
||||||
|
import { Head, useRouter } from 'tuono'
|
||||||
|
import Navbar from '../components/navbar'
|
||||||
|
|
||||||
|
import '@mantine/core/styles.css'
|
||||||
|
import '@mantine/code-highlight/styles.css'
|
||||||
|
import Sidebar from '../components/sidebar'
|
||||||
|
|
||||||
|
interface RootRouteProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
primaryColor: 'violet',
|
||||||
|
primaryShade: { light: 6, dark: 9 },
|
||||||
|
fontFamily: 'Roboto',
|
||||||
|
respectReducedMotion: true,
|
||||||
|
fontSizes: {
|
||||||
|
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
xs: '16px',
|
||||||
|
sm: '16px',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
dark: [
|
||||||
|
'#d5d7e0',
|
||||||
|
'#acaebf',
|
||||||
|
'#8c8fa3',
|
||||||
|
'#666980',
|
||||||
|
'#4d4f66',
|
||||||
|
'#34354a',
|
||||||
|
'#2b2c3d',
|
||||||
|
'#1d1e30',
|
||||||
|
'#0c0d21',
|
||||||
|
'#01010a',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
headings: {
|
||||||
|
sizes: {
|
||||||
|
h1: {
|
||||||
|
fontSize: '48px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||||
|
const [opened, { toggle }] = useDisclosure()
|
||||||
|
|
||||||
|
const { pathname } = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<ColorSchemeScript />
|
||||||
|
</Head>
|
||||||
|
<MantineProvider theme={theme}>
|
||||||
|
<AppShell
|
||||||
|
header={{ height: 60 }}
|
||||||
|
navbar={{
|
||||||
|
width: 300,
|
||||||
|
breakpoint: 'sm',
|
||||||
|
collapsed: { mobile: !opened },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Navbar opened={opened} toggle={toggle} />
|
||||||
|
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||||
|
{children}
|
||||||
|
</AppShell>
|
||||||
|
</MantineProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { AppShell, Container } from '@mantine/core'
|
||||||
|
import MdxProvider from '../../components/mdx-provider'
|
||||||
|
import EditPage from '../../components/edit-page'
|
||||||
|
|
||||||
|
interface RootRouteProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<AppShell.Main>
|
||||||
|
<Container component="article" p={20}>
|
||||||
|
<MdxProvider>{children}</MdxProvider>
|
||||||
|
<EditPage />
|
||||||
|
</Container>
|
||||||
|
</AppShell.Main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Contributing</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="✨ Contributing" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
The project is massive - if you like it do consider to contribute!
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
The `tuono` project can mostly be split by the following sub-domains:
|
||||||
|
|
||||||
|
- The CLI
|
||||||
|
- The rust backend
|
||||||
|
- The react frontend
|
||||||
|
- The documentation website (which is written with tuono 🚀)
|
||||||
|
|
||||||
|
To check what are the knowledge requirement for each domain check the
|
||||||
|
[requirements](#requirements) section below.
|
||||||
|
|
||||||
|
Currently I'm keeping a private dashboard to prioritize new features and bug fixes but if you
|
||||||
|
want to propose something please open a new issue on github or reach me using
|
||||||
|
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
|
||||||
|
on twitter (X) DMs `@valerioageno`, [linkedin](https://www.linkedin.com/in/valerioageno)
|
||||||
|
and discord `@__v__v__`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
It's not strictly required to know both React (& typescript) and Rust (even though it
|
||||||
|
would be a great nice to have).
|
||||||
|
|
||||||
|
Without taking into account specific cases we can mostly split the domain requirements by:
|
||||||
|
|
||||||
|
- The `CLI` needs Rust knowledge (even though a couple of scenario might need also Typescript)
|
||||||
|
- The Backend needs just `Rust`
|
||||||
|
- The Frontend needs just `React` & `Typescript`
|
||||||
|
- The documentation website needs just `React` & `Typescript` (or even less since most of the
|
||||||
|
code is markdown).
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Getting started</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="Getting started" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Getting started
|
||||||
|
|
||||||
|
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
|
||||||
|
|
||||||
|
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
|
||||||
|
> `tuono` CLI yet.
|
||||||
|
|
||||||
|
To list all the available commands run `tuono -h`.
|
||||||
|
|
||||||
|
To create a new project run `tuono new [NAME]` (optionally you can pass the --template (or -t) flag - check the examples folder).
|
||||||
|
|
||||||
|
Then to run the local development environment run inside the project folder `tuono dev`
|
||||||
|
|
||||||
|
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
|
||||||
|
|
||||||
|
Now to execute it just run `cargo run --release`.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Documentation</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs from '../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs></Breadcrumbs>
|
||||||
|
|
||||||
|
# Tuono
|
||||||
|
|
||||||
|
**Tuono is a fullstack framework for building React applications using Rust as
|
||||||
|
backend.**
|
||||||
|
|
||||||
|
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be fullstack right? (no)**
|
||||||
|
|
||||||
|
Tuono is a fullstack React framework with the server side written in Rust.
|
||||||
|
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
|
||||||
|
React is still React - it is just superpowered.
|
||||||
|
|
||||||
|
**Rust is an hard language then writing server side code is hard as well right? (no again)**
|
||||||
|
|
||||||
|
Tuono provides a collection of utilities to handle the server side code seamlessly with
|
||||||
|
the React code. Each server side route is managed with a separate file alongside the React route.
|
||||||
|
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Some of its features include:
|
||||||
|
|
||||||
|
- 🟦 Full typescript support
|
||||||
|
- 🌐 File system based routing
|
||||||
|
- 🔥 Hot Module Reload
|
||||||
|
- 🍭 SCSS/CSS modules
|
||||||
|
- 🧬 Server Side Rendering
|
||||||
|
- 🧵 Multi thread backend
|
||||||
|
- ⌨️ MDX support
|
||||||
|
- ⚙️ Build optimizations
|
||||||
|
- Custom APIs\*
|
||||||
|
- Image optimization\*
|
||||||
|
- Server streamed content\*
|
||||||
|
|
||||||
|
> \*development in progress
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Installation</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="Installation" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
`Tuono` is a development environment built in rust and typescript that outputs a website written with both the
|
||||||
|
languages. Since that you need the following tools installed on your computer to work with it:
|
||||||
|
|
||||||
|
- `Rust` - The rust programming language toolchain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
|
||||||
|
- `Cargo` - The rust package manager
|
||||||
|
- `NodeJs` - The Javascript runtime (Go [here](https://nodejs.org/en/download/package-manager) for installing both `nodejs` and `npm`)
|
||||||
|
- `npm` or `yarn` or `pnpm` - A javascript package manager
|
||||||
|
|
||||||
|
> NodeJs is needed just for the development environment. The final output will run on a rust server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
The tuono `CLI` is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo install tuono
|
||||||
|
```
|
||||||
|
|
||||||
|
To check that is correctly installed run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tuono --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `tuono -h` to see all the available commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
The react/rust fullstack framework
|
||||||
|
|
||||||
|
Usage: tuono <COMMAND>
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
dev Start the development environment
|
||||||
|
build Build the production assets
|
||||||
|
new Scaffold a new project
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Print help
|
||||||
|
-V, --version Print version
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Routing</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="Routing" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Routing index
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Routing</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="Routing" href="/documentation/routing" />
|
||||||
|
<Element label="Project structure" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Project structure
|
||||||
|
|
||||||
|
Todo
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - Tutorial intro</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||||
|
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Element label="Tutorial" />
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
# Tutorial
|
||||||
|
|
||||||
|
You can find the tutorial [here](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
||||||
|
|
||||||
|
Other tasks have the priority right now. The tutorial will be ported here as soon as possible.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Head } from 'tuono'
|
||||||
|
import Hero from '../components/hero'
|
||||||
|
|
||||||
|
export default function IndexPage(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Tuono - The react/rust fullstack framework</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="The technologies we love seamessly working together to unleash the highest web performance ever met on react"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<Hero />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono"
|
name = "tuono"
|
||||||
version = "0.4.3"
|
version = "0.9.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||||
description = "The react/rust fullstack framework"
|
description = "The react/rust fullstack framework"
|
||||||
@@ -30,4 +30,5 @@ glob = "0.3.1"
|
|||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
fs_extra = "1.3.0"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
use glob::glob;
|
||||||
|
use glob::GlobError;
|
||||||
|
use std::collections::{hash_map::Entry, HashMap};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Child;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use crate::route::Route;
|
||||||
|
|
||||||
|
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
|
||||||
|
const IGNORE_FILES: [&str; 1] = ["__root"];
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct App {
|
||||||
|
pub route_map: HashMap<String, Route>,
|
||||||
|
pub base_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let base_path = std::env::current_dir().unwrap();
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
route_map: HashMap::new(),
|
||||||
|
base_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.collect_routes();
|
||||||
|
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_routes(&mut self) {
|
||||||
|
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
|
||||||
|
.expect("Failed to read glob pattern")
|
||||||
|
.for_each(|entry| {
|
||||||
|
if self.should_collect_route(&entry) {
|
||||||
|
self.collect_route(entry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
|
||||||
|
let file_extension = entry.as_ref().unwrap().extension().unwrap();
|
||||||
|
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
|
||||||
|
|
||||||
|
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if IGNORE_FILES.iter().any(|val| val == &file_name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
||||||
|
let entry = path_buf.unwrap();
|
||||||
|
let base_path_str = self.base_path.to_str().unwrap();
|
||||||
|
let path = entry
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.replace(&format!("{base_path_str}/src/routes"), "")
|
||||||
|
.replace(".rs", "")
|
||||||
|
.replace(".mdx", "")
|
||||||
|
.replace(".tsx", "");
|
||||||
|
|
||||||
|
if entry.extension().unwrap() == "rs" {
|
||||||
|
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||||
|
let mut route = Route::new(path);
|
||||||
|
route.update_axum_info();
|
||||||
|
route_map.insert(route);
|
||||||
|
} else {
|
||||||
|
let route = self.route_map.get_mut(&path).unwrap();
|
||||||
|
route.update_axum_info();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||||
|
let route = Route::new(path);
|
||||||
|
route_map.insert(route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_dynamic_routes(&self) -> bool {
|
||||||
|
self.route_map.iter().any(|(_, route)| route.is_dynamic)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_react_prod(&self) {
|
||||||
|
Command::new("./node_modules/.bin/tuono-build-prod")
|
||||||
|
.output()
|
||||||
|
.expect("Failed to build the react source");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_rust_server(&self) -> Child {
|
||||||
|
Command::new("cargo")
|
||||||
|
.arg("run")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.expect("Failed to run the rust server")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_collect_routes() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
let results = [
|
||||||
|
("/index", "index"),
|
||||||
|
("/about", "about"),
|
||||||
|
("/posts/index", "posts_index"),
|
||||||
|
("/posts/[post]", "posts_dyn_post"),
|
||||||
|
("/posts/UPPERCASE", "posts_uppercase"),
|
||||||
|
];
|
||||||
|
|
||||||
|
results.into_iter().for_each(|(path, module_import)| {
|
||||||
|
assert_eq!(
|
||||||
|
app.route_map
|
||||||
|
.get(path)
|
||||||
|
.unwrap()
|
||||||
|
.axum_info
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.module_import,
|
||||||
|
String::from(module_import)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_create_multi_level_axum_paths() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/any-post.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
let results = [
|
||||||
|
("/index", "/"),
|
||||||
|
("/about", "/about"),
|
||||||
|
("/posts/index", "/posts"),
|
||||||
|
("/posts/any-post", "/posts/any-post"),
|
||||||
|
("/posts/[post]", "/posts/:post"),
|
||||||
|
];
|
||||||
|
|
||||||
|
results.into_iter().for_each(|(path, expected_path)| {
|
||||||
|
assert_eq!(
|
||||||
|
app.route_map
|
||||||
|
.get(path)
|
||||||
|
.unwrap()
|
||||||
|
.axum_info
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.axum_route,
|
||||||
|
String::from(expected_path)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_whitelisted_extensions() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/about.css",
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.scss",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/index.sass",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes.into_iter().for_each(|route| {
|
||||||
|
if app.should_collect_route(&Ok(PathBuf::from(route))) {
|
||||||
|
app.collect_route(Ok(PathBuf::from(route)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(app.route_map.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_ignore_whitelisted_files() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/__root.tsx",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/__root.tsx",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes.into_iter().for_each(|route| {
|
||||||
|
if app.should_collect_route(&Ok(PathBuf::from(route))) {
|
||||||
|
app.collect_route(Ok(PathBuf::from(route)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(app.route_map.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_correctly_parse_routes_with_server_handler() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/about.tsx",
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.tsx",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
let results = [("/about", true), ("/index", false)];
|
||||||
|
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|(path, expected_has_server_handler)| {
|
||||||
|
if expected_has_server_handler {
|
||||||
|
assert!(app.route_map.get(path).unwrap().axum_info.is_some())
|
||||||
|
} else {
|
||||||
|
assert!(app.route_map.get(path).unwrap().axum_info.is_none())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn has_dynamic_routes_works() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
assert!(app.has_dynamic_routes());
|
||||||
|
|
||||||
|
let mut app2 = App::new();
|
||||||
|
app2.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/[post].rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app2.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
assert!(app2.has_dynamic_routes());
|
||||||
|
|
||||||
|
let mut app3 = App::new();
|
||||||
|
app3.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
|
let routes = [
|
||||||
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| app3.collect_route(Ok(PathBuf::from(route))));
|
||||||
|
|
||||||
|
assert!(!app3.has_dynamic_routes())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
use fs_extra::dir::{copy, CopyOptions};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::mode::Mode;
|
||||||
|
use crate::scaffold_project;
|
||||||
|
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
||||||
|
use crate::watch;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Actions {
|
||||||
|
/// Start the development environment
|
||||||
|
Dev,
|
||||||
|
/// Build the production assets
|
||||||
|
Build {
|
||||||
|
#[arg(short, long = "static")]
|
||||||
|
/// Statically generate the website HTML
|
||||||
|
ssg: bool,
|
||||||
|
},
|
||||||
|
/// Scaffold a new project
|
||||||
|
New {
|
||||||
|
/// The folder in which load the project. Default is the current directory.
|
||||||
|
folder_name: Option<String>,
|
||||||
|
/// The template to use to scaffold the project. The template should match one of the tuono
|
||||||
|
/// examples
|
||||||
|
#[arg(short, long)]
|
||||||
|
template: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about = "The react/rust fullstack framework")]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: Actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
||||||
|
check_tuono_folder()?;
|
||||||
|
bundle_axum_source(mode)?;
|
||||||
|
create_client_entry_files()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app() -> std::io::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
Actions::Dev => {
|
||||||
|
init_tuono_folder(Mode::Dev)?;
|
||||||
|
watch::watch().unwrap();
|
||||||
|
}
|
||||||
|
Actions::Build { ssg } => {
|
||||||
|
init_tuono_folder(Mode::Prod)?;
|
||||||
|
let app = App::new();
|
||||||
|
|
||||||
|
if ssg && app.has_dynamic_routes() {
|
||||||
|
// TODO: allow dynamic routes static generation
|
||||||
|
println!("Cannot statically build dynamic routes");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.build_react_prod();
|
||||||
|
|
||||||
|
if ssg {
|
||||||
|
println!("SSG: generation started");
|
||||||
|
|
||||||
|
let static_dir = PathBuf::from("out/static");
|
||||||
|
|
||||||
|
if static_dir.is_dir() {
|
||||||
|
std::fs::remove_dir_all(&static_dir)
|
||||||
|
.expect("Failed to clear the out/static folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir(&static_dir).expect("Failed to create static output dir");
|
||||||
|
|
||||||
|
copy(
|
||||||
|
"./out/client",
|
||||||
|
static_dir,
|
||||||
|
&CopyOptions::new().overwrite(true).content_only(true),
|
||||||
|
)
|
||||||
|
.expect("Failed to clone assets into static output folder");
|
||||||
|
|
||||||
|
let mut rust_server = app.run_rust_server();
|
||||||
|
|
||||||
|
let reqwest = reqwest::blocking::Client::builder()
|
||||||
|
.user_agent("")
|
||||||
|
.build()
|
||||||
|
.expect("Failed to build reqwest client");
|
||||||
|
|
||||||
|
// Wait for server
|
||||||
|
let mut is_server_ready = false;
|
||||||
|
|
||||||
|
while !is_server_ready {
|
||||||
|
if reqwest.get("http://localhost:3000").send().is_ok() {
|
||||||
|
is_server_ready = true
|
||||||
|
}
|
||||||
|
// TODO: add maximum tries
|
||||||
|
sleep(Duration::from_secs(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, route) in app.route_map {
|
||||||
|
route.save_ssg_html(&reqwest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close server
|
||||||
|
let _ = rust_server.kill();
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Build successfully finished");
|
||||||
|
}
|
||||||
|
Actions::New {
|
||||||
|
folder_name,
|
||||||
|
template,
|
||||||
|
} => {
|
||||||
|
scaffold_project::create_new_project(folder_name, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+5
-64
@@ -1,66 +1,7 @@
|
|||||||
use clap::{Parser, Subcommand};
|
mod app;
|
||||||
use std::process::Command;
|
pub mod cli;
|
||||||
|
mod mode;
|
||||||
mod source_builder;
|
mod route;
|
||||||
use source_builder::{bundle_axum_source, create_client_entry_files};
|
|
||||||
|
|
||||||
use crate::source_builder::{check_tuono_folder, Mode};
|
|
||||||
|
|
||||||
mod scaffold_project;
|
mod scaffold_project;
|
||||||
|
mod source_builder;
|
||||||
mod watch;
|
mod watch;
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
enum Actions {
|
|
||||||
/// Start the development environment
|
|
||||||
Dev,
|
|
||||||
/// Build the production assets
|
|
||||||
Build,
|
|
||||||
/// Scaffold a new project
|
|
||||||
New {
|
|
||||||
/// The folder in which load the project. Default is the current directory.
|
|
||||||
folder_name: Option<String>,
|
|
||||||
/// The template to use to scaffold the project. The template should match one of the tuono
|
|
||||||
/// examples
|
|
||||||
#[arg(short, long)]
|
|
||||||
template: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about = "The react/rust fullstack framework")]
|
|
||||||
struct Args {
|
|
||||||
#[command(subcommand)]
|
|
||||||
action: Actions,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
|
||||||
check_tuono_folder()?;
|
|
||||||
bundle_axum_source(mode)?;
|
|
||||||
create_client_entry_files()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cli() -> std::io::Result<()> {
|
|
||||||
let args = Args::parse();
|
|
||||||
|
|
||||||
match args.action {
|
|
||||||
Actions::Dev => {
|
|
||||||
init_tuono_folder(Mode::Dev)?;
|
|
||||||
watch::watch().unwrap();
|
|
||||||
}
|
|
||||||
Actions::Build => {
|
|
||||||
init_tuono_folder(Mode::Prod)?;
|
|
||||||
let mut vite_build = Command::new("./node_modules/.bin/tuono-build-prod");
|
|
||||||
let _ = &vite_build.output()?;
|
|
||||||
}
|
|
||||||
Actions::New {
|
|
||||||
folder_name,
|
|
||||||
template,
|
|
||||||
} => {
|
|
||||||
scaffold_project::create_new_project(folder_name, template);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use tuono::cli;
|
use tuono::cli::app;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
cli().unwrap();
|
app().expect("Failed to start the CLI")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub enum Mode {
|
||||||
|
Prod,
|
||||||
|
Dev,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
pub fn as_str<'a>(&self) -> &'a str {
|
||||||
|
if *self == Mode::Dev {
|
||||||
|
return "Mode::Dev";
|
||||||
|
}
|
||||||
|
"Mode::Prod"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_correctly_print_the_mode_as_str() {
|
||||||
|
let dev = Mode::Dev.as_str();
|
||||||
|
let prod = Mode::Prod.as_str();
|
||||||
|
assert_eq!(dev, "Mode::Dev");
|
||||||
|
assert_eq!(prod, "Mode::Prod");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
use fs_extra::dir::create_all;
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use reqwest::Url;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn has_dynamic_path(route: &str) -> bool {
|
||||||
|
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||||
|
regex.is_match(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct AxumInfo {
|
||||||
|
// Path for importing the module
|
||||||
|
pub module_import: String,
|
||||||
|
// path for the the axum router
|
||||||
|
pub axum_route: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AxumInfo {
|
||||||
|
pub fn new(path: String) -> Self {
|
||||||
|
// Remove first slash
|
||||||
|
let mut module = path.chars();
|
||||||
|
module.next();
|
||||||
|
|
||||||
|
let axum_route = path.replace("/index", "");
|
||||||
|
|
||||||
|
if axum_route.is_empty() {
|
||||||
|
return AxumInfo {
|
||||||
|
module_import: module.as_str().to_string().replace('/', "_"),
|
||||||
|
axum_route: "/".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_dynamic_path(&path) {
|
||||||
|
return AxumInfo {
|
||||||
|
module_import: module
|
||||||
|
.as_str()
|
||||||
|
.to_string()
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace('[', "dyn_")
|
||||||
|
.replace(']', ""),
|
||||||
|
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AxumInfo {
|
||||||
|
module_import: module.as_str().to_string().replace('/', "_").to_lowercase(),
|
||||||
|
axum_route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct Route {
|
||||||
|
path: String,
|
||||||
|
pub is_dynamic: bool,
|
||||||
|
pub axum_info: Option<AxumInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Route {
|
||||||
|
pub fn new(cleaned_path: String) -> Self {
|
||||||
|
Route {
|
||||||
|
path: cleaned_path.clone(),
|
||||||
|
axum_info: None,
|
||||||
|
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_axum_info(&mut self) {
|
||||||
|
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_ssg_html(&self, reqwest: &Client) {
|
||||||
|
let path = &self.path.replace("index", "");
|
||||||
|
|
||||||
|
let mut response = reqwest
|
||||||
|
.get(format!("http://localhost:3000{path}"))
|
||||||
|
.send()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let file_path = self.html_file_path();
|
||||||
|
|
||||||
|
let parent_dir = file_path.parent().unwrap();
|
||||||
|
|
||||||
|
if !parent_dir.is_dir() {
|
||||||
|
create_all(parent_dir, false).expect("Failed to create parent directories");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = File::create(file_path).expect("Failed to create the HTML file");
|
||||||
|
|
||||||
|
io::copy(&mut response, &mut file).expect("Failed to write the HTML on the file");
|
||||||
|
|
||||||
|
// Saving also the server response
|
||||||
|
if self.axum_info.is_some() {
|
||||||
|
let data_file_path =
|
||||||
|
PathBuf::from(&format!("out/static/__tuono/data{}/data.json", path));
|
||||||
|
|
||||||
|
let data_parent_dir = data_file_path.parent().unwrap();
|
||||||
|
|
||||||
|
if !data_parent_dir.is_dir() {
|
||||||
|
create_all(data_parent_dir, false)
|
||||||
|
.expect("Failed to create data parent directories");
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = Url::parse("http://localhost:3000/__tuono/data/data.json").unwrap();
|
||||||
|
|
||||||
|
let path = if path == "/" { "" } else { path };
|
||||||
|
|
||||||
|
let pathname = &format!("/__tuono/data{path}/data.json");
|
||||||
|
|
||||||
|
let url = base
|
||||||
|
.join(pathname)
|
||||||
|
.expect("Failed to build the reqwest URL");
|
||||||
|
|
||||||
|
let mut response = reqwest.get(url).send().unwrap();
|
||||||
|
|
||||||
|
let mut data_file =
|
||||||
|
File::create(data_file_path).expect("Failed to create the JSON file");
|
||||||
|
|
||||||
|
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn html_file_path(&self) -> PathBuf {
|
||||||
|
let cleaned_path = self.path.replace("index", "");
|
||||||
|
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_find_dynamic_paths() {
|
||||||
|
let routes = [
|
||||||
|
("/home/user/Documents/tuono/src/routes/about.rs", false),
|
||||||
|
("/home/user/Documents/tuono/src/routes/index.rs", false),
|
||||||
|
(
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
routes
|
||||||
|
.into_iter()
|
||||||
|
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_correctly_create_the_axum_infos() {
|
||||||
|
let info = AxumInfo::new("/index".to_string());
|
||||||
|
|
||||||
|
assert_eq!(info.axum_route, "/");
|
||||||
|
assert_eq!(info.module_import, "index");
|
||||||
|
|
||||||
|
let dyn_info = AxumInfo::new("/[posts]".to_string());
|
||||||
|
|
||||||
|
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||||
|
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_define_the_correct_html_build_path() {
|
||||||
|
let routes = [
|
||||||
|
("/index", "out/static/index.html"),
|
||||||
|
("/documentation", "out/static/documentation/index.html"),
|
||||||
|
(
|
||||||
|
"/documentation/routing",
|
||||||
|
"out/static/documentation/routing/index.html",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (path, html) in routes {
|
||||||
|
let route = Route::new(path.to_string());
|
||||||
|
|
||||||
|
assert_eq!(route.html_file_path(), PathBuf::from(html))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,13 @@
|
|||||||
use glob::glob;
|
|
||||||
use glob::GlobError;
|
|
||||||
use regex::Regex;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
use crate::app::App;
|
||||||
pub enum Mode {
|
use crate::mode::Mode;
|
||||||
Prod,
|
use crate::route::AxumInfo;
|
||||||
Dev,
|
use crate::route::Route;
|
||||||
}
|
|
||||||
|
|
||||||
impl Mode {
|
|
||||||
pub fn as_str<'a>(&self) -> &'a str {
|
|
||||||
if *self == Mode::Dev {
|
|
||||||
return "Mode::Dev";
|
|
||||||
}
|
|
||||||
"Mode::Prod"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
|
pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
|
||||||
// Do not manually update this file
|
// Do not manually update this file
|
||||||
@@ -47,7 +33,8 @@ pub const AXUM_ENTRY_POINT: &str = r##"
|
|||||||
// File automatically generated
|
// File automatically generated
|
||||||
// Do not manually change it
|
// Do not manually change it
|
||||||
|
|
||||||
use tuono_lib::{tokio, Mode, Server, axum::Router, axum::routing::get};
|
use tuono_lib::{tokio, Mode, Server, axum::Router};
|
||||||
|
// AXUM_GET_ROUTE_HANDLER
|
||||||
|
|
||||||
const MODE: Mode = /*MODE*/;
|
const MODE: Mode = /*MODE*/;
|
||||||
|
|
||||||
@@ -63,91 +50,9 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
const ROOT_FOLDER: &str = "src/routes";
|
const ROUTE_FOLDER: &str = "src/routes";
|
||||||
const DEV_FOLDER: &str = ".tuono";
|
const DEV_FOLDER: &str = ".tuono";
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
struct Route {
|
|
||||||
/// Every module import is the path with a _ instead of /
|
|
||||||
pub module_import: String,
|
|
||||||
pub axum_route: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_dynamic_path(route: &str) -> bool {
|
|
||||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
|
||||||
regex.is_match(route)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Route {
|
|
||||||
pub fn new(path: &str) -> Self {
|
|
||||||
let route_name = path.replace(".rs", "");
|
|
||||||
// Remove first slash
|
|
||||||
let mut module = route_name.as_str().chars();
|
|
||||||
module.next();
|
|
||||||
|
|
||||||
let axum_route = path.replace("/index.rs", "").replace(".rs", "");
|
|
||||||
|
|
||||||
if axum_route.is_empty() {
|
|
||||||
return Route {
|
|
||||||
module_import: module.as_str().to_string().replace('/', "_"),
|
|
||||||
axum_route: "/".to_string(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if has_dynamic_path(&route_name) {
|
|
||||||
return Route {
|
|
||||||
module_import: module
|
|
||||||
.as_str()
|
|
||||||
.to_string()
|
|
||||||
.replace('/', "_")
|
|
||||||
.replace('[', "dyn_")
|
|
||||||
.replace(']', ""),
|
|
||||||
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Route {
|
|
||||||
module_import: module.as_str().to_string().replace('/', "_").to_lowercase(),
|
|
||||||
axum_route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SourceBuilder {
|
|
||||||
route_map: HashMap<PathBuf, Route>,
|
|
||||||
base_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let base_path = std::env::current_dir().unwrap();
|
|
||||||
|
|
||||||
SourceBuilder {
|
|
||||||
route_map: HashMap::new(),
|
|
||||||
base_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_routes(&mut self) {
|
|
||||||
glob(self.base_path.join("src/routes/**/*.rs").to_str().unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.for_each(|entry| self.collect_route(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
|
||||||
let entry = path_buf.unwrap();
|
|
||||||
let base_path_str = self.base_path.to_str().unwrap();
|
|
||||||
let path = entry
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace(&format!("{base_path_str}/src/routes"), "");
|
|
||||||
|
|
||||||
let route = Route::new(&path);
|
|
||||||
|
|
||||||
self.route_map.insert(PathBuf::from(path), route);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_main_file(base_path: &Path, bundled_file: &String) {
|
fn create_main_file(base_path: &Path, bundled_file: &String) {
|
||||||
let mut data_file =
|
let mut data_file =
|
||||||
fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed");
|
fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed");
|
||||||
@@ -157,37 +62,44 @@ fn create_main_file(base_path: &Path, bundled_file: &String) {
|
|||||||
.expect("write failed");
|
.expect("write failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_routes_declaration(routes: &HashMap<PathBuf, Route>) -> String {
|
fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||||
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
|
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
|
||||||
|
|
||||||
for (_, route) in routes.iter() {
|
for (_, route) in routes.iter() {
|
||||||
let Route {
|
let Route { axum_info, .. } = &route;
|
||||||
axum_route,
|
|
||||||
module_import,
|
|
||||||
} = &route;
|
|
||||||
|
|
||||||
route_declarations.push_str(&format!(
|
if axum_info.is_some() {
|
||||||
r#".route("{axum_route}", get({module_import}::route))"#
|
let AxumInfo {
|
||||||
));
|
axum_route,
|
||||||
route_declarations.push_str(&format!(
|
module_import,
|
||||||
r#".route("/__tuono/data{axum_route}", get({module_import}::api))"#
|
} = axum_info.as_ref().unwrap();
|
||||||
));
|
|
||||||
|
route_declarations.push_str(&format!(
|
||||||
|
r#".route("{axum_route}", get({module_import}::route))"#
|
||||||
|
));
|
||||||
|
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||||
|
route_declarations.push_str(&format!(
|
||||||
|
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route_declarations
|
route_declarations
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_modules_declaration(routes: &HashMap<PathBuf, Route>) -> String {
|
fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
|
||||||
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
|
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
|
||||||
|
|
||||||
for (path, route) in routes.iter() {
|
for (path, route) in routes.iter() {
|
||||||
let module_name = &route.module_import;
|
if route.axum_info.is_some() {
|
||||||
let path_str = path.to_str().unwrap();
|
let AxumInfo { module_import, .. } = route.axum_info.as_ref().unwrap();
|
||||||
route_declarations.push_str(&format!(
|
|
||||||
r#"#[path="../{ROOT_FOLDER}{path_str}"]
|
route_declarations.push_str(&format!(
|
||||||
mod {module_name};
|
r#"#[path="../{ROUTE_FOLDER}{path}.rs"]
|
||||||
"#
|
mod {module_import};
|
||||||
))
|
"#
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route_declarations
|
route_declarations
|
||||||
@@ -196,28 +108,41 @@ mod {module_name};
|
|||||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
||||||
let base_path = std::env::current_dir().unwrap();
|
let base_path = std::env::current_dir().unwrap();
|
||||||
|
|
||||||
let mut source_builder = SourceBuilder::new();
|
let app = App::new();
|
||||||
|
|
||||||
source_builder.collect_routes();
|
let bundled_file = generate_axum_source(&app, mode);
|
||||||
|
|
||||||
let bundled_file = generate_axum_source(&source_builder, mode);
|
|
||||||
|
|
||||||
create_main_file(&base_path, &bundled_file);
|
create_main_file(&base_path, &bundled_file);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_axum_source(source_builder: &SourceBuilder, mode: Mode) -> String {
|
fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||||
AXUM_ENTRY_POINT
|
let src = AXUM_ENTRY_POINT
|
||||||
.replace(
|
.replace(
|
||||||
"// ROUTE_BUILDER\n",
|
"// ROUTE_BUILDER\n",
|
||||||
&create_routes_declaration(&source_builder.route_map),
|
&create_routes_declaration(&app.route_map),
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"// MODULE_IMPORTS\n",
|
"// MODULE_IMPORTS\n",
|
||||||
&create_modules_declaration(&source_builder.route_map),
|
&create_modules_declaration(&app.route_map),
|
||||||
)
|
)
|
||||||
.replace("/*MODE*/", mode.as_str())
|
.replace("/*MODE*/", mode.as_str());
|
||||||
|
|
||||||
|
let has_server_handlers = app
|
||||||
|
.route_map
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, route)| route.axum_info.is_some())
|
||||||
|
.collect::<Vec<(&String, &Route)>>()
|
||||||
|
.is_empty();
|
||||||
|
|
||||||
|
if !has_server_handlers {
|
||||||
|
return src.replace(
|
||||||
|
"// AXUM_GET_ROUTE_HANDLER",
|
||||||
|
"use tuono_lib::axum::routing::get;",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
src
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_tuono_folder() -> io::Result<()> {
|
pub fn check_tuono_folder() -> io::Result<()> {
|
||||||
@@ -246,103 +171,9 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_find_dynamic_paths() {
|
|
||||||
let routes = [
|
|
||||||
("/home/user/Documents/tuono/src/routes/about.rs", false),
|
|
||||||
("/home/user/Documents/tuono/src/routes/index.rs", false),
|
|
||||||
(
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
routes
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_collect_routes() {
|
|
||||||
let mut source_builder = SourceBuilder::new();
|
|
||||||
source_builder.base_path = "/home/user/Documents/tuono".into();
|
|
||||||
|
|
||||||
let routes = [
|
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
|
|
||||||
];
|
|
||||||
|
|
||||||
routes
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route))));
|
|
||||||
|
|
||||||
let results = [
|
|
||||||
("/index.rs", "index"),
|
|
||||||
("/about.rs", "about"),
|
|
||||||
("/posts/index.rs", "posts_index"),
|
|
||||||
("/posts/[post].rs", "posts_dyn_post"),
|
|
||||||
("/posts/UPPERCASE.rs", "posts_uppercase"),
|
|
||||||
];
|
|
||||||
|
|
||||||
results.into_iter().for_each(|(path, module_import)| {
|
|
||||||
assert_eq!(
|
|
||||||
source_builder
|
|
||||||
.route_map
|
|
||||||
.get(&PathBuf::from(path))
|
|
||||||
.unwrap()
|
|
||||||
.module_import,
|
|
||||||
String::from(module_import)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn should_create_multi_level_axum_paths() {
|
|
||||||
let mut source_builder = SourceBuilder::new();
|
|
||||||
source_builder.base_path = "/home/user/Documents/tuono".into();
|
|
||||||
|
|
||||||
let routes = [
|
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/any-post.rs",
|
|
||||||
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
|
||||||
];
|
|
||||||
|
|
||||||
routes
|
|
||||||
.into_iter()
|
|
||||||
.for_each(|route| source_builder.collect_route(Ok(PathBuf::from(route))));
|
|
||||||
|
|
||||||
let results = [
|
|
||||||
("/index.rs", "/"),
|
|
||||||
("/about.rs", "/about"),
|
|
||||||
("/posts/index.rs", "/posts"),
|
|
||||||
("/posts/any-post.rs", "/posts/any-post"),
|
|
||||||
("/posts/[post].rs", "/posts/:post"),
|
|
||||||
];
|
|
||||||
|
|
||||||
results.into_iter().for_each(|(path, expected_path)| {
|
|
||||||
assert_eq!(
|
|
||||||
source_builder
|
|
||||||
.route_map
|
|
||||||
.get(&PathBuf::from(path))
|
|
||||||
.unwrap()
|
|
||||||
.axum_route,
|
|
||||||
String::from(expected_path)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_set_the_correct_mode() {
|
fn should_set_the_correct_mode() {
|
||||||
let source_builder = SourceBuilder::new();
|
let source_builder = App::new();
|
||||||
|
|
||||||
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
||||||
assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;"));
|
assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;"));
|
||||||
@@ -353,10 +184,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_print_the_mode_as_str() {
|
fn should_not_load_the_axum_get_function() {
|
||||||
let dev = Mode::Dev.as_str();
|
let source_builder = App::new();
|
||||||
let prod = Mode::Prod.as_str();
|
|
||||||
assert_eq!(dev, "Mode::Dev");
|
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
||||||
assert_eq!(prod, "Mode::Prod");
|
assert!(!dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_load_the_axum_get_function() {
|
||||||
|
let mut source_builder = App::new();
|
||||||
|
|
||||||
|
let mut route = Route::new(String::from("index.tsx"));
|
||||||
|
route.update_axum_info();
|
||||||
|
|
||||||
|
source_builder
|
||||||
|
.route_map
|
||||||
|
.insert(String::from("index.rs"), route);
|
||||||
|
|
||||||
|
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
||||||
|
assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use watchexec::Watchexec;
|
|||||||
use watchexec_signals::Signal;
|
use watchexec_signals::Signal;
|
||||||
use watchexec_supervisor::job::{start_job, Job};
|
use watchexec_supervisor::job::{start_job, Job};
|
||||||
|
|
||||||
use crate::source_builder::{bundle_axum_source, Mode};
|
use crate::mode::Mode;
|
||||||
|
use crate::source_builder::bundle_axum_source;
|
||||||
|
|
||||||
fn watch_react_src() -> Job {
|
fn watch_react_src() -> Job {
|
||||||
start_job(Arc::new(Command {
|
start_job(Arc::new(Command {
|
||||||
@@ -62,12 +63,13 @@ pub async fn watch() -> Result<()> {
|
|||||||
|
|
||||||
for event in action.events.iter() {
|
for event in action.events.iter() {
|
||||||
for path in event.paths() {
|
for path in event.paths() {
|
||||||
if path.0.to_string_lossy().ends_with(".rs") {
|
let file_path = path.0.to_string_lossy();
|
||||||
|
if file_path.ends_with(".rs") {
|
||||||
should_reload_rust_server = true
|
should_reload_rust_server = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either tsx and jsx
|
// Either tsx, jsx and mdx
|
||||||
if path.0.to_string_lossy().ends_with("sx") {
|
if file_path.ends_with("sx") || file_path.ends_with("mdx") {
|
||||||
should_reload_ssr_bundle = true
|
should_reload_ssr_bundle = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono_lib"
|
name = "tuono_lib"
|
||||||
version = "0.4.3"
|
version = "0.9.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||||
description = "The react/rust fullstack framework"
|
description = "The react/rust fullstack framework"
|
||||||
@@ -25,13 +25,14 @@ tokio = { version = "1.37.0", features = ["full"] }
|
|||||||
serde = { version = "1.0.202", features = ["derive"] }
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
erased-serde = "0.4.5"
|
erased-serde = "0.4.5"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
serde_urlencoded = "0.7.1"
|
||||||
reqwest = {version = "0.12.4", features = ["json", "stream"]}
|
reqwest = {version = "0.12.4", features = ["json", "stream"]}
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
regex = "1.10.5"
|
regex = "1.10.5"
|
||||||
either = "1.13.0"
|
either = "1.13.0"
|
||||||
tower-http = {version = "0.5.2", features = ["fs"]}
|
tower-http = {version = "0.5.2", features = ["fs"]}
|
||||||
|
|
||||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.4.3"}
|
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.9.2"}
|
||||||
# Match the same version used by axum
|
# Match the same version used by axum
|
||||||
tokio-tungstenite = "0.21.0"
|
tokio-tungstenite = "0.21.0"
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ pub async fn catch_all(
|
|||||||
Path(params): Path<HashMap<String, String>>,
|
Path(params): Path<HashMap<String, String>>,
|
||||||
request: Request,
|
request: Request,
|
||||||
) -> Html<String> {
|
) -> Html<String> {
|
||||||
let pathname = &request.uri();
|
let pathname = request.uri();
|
||||||
let headers = &request.headers();
|
let headers = request.headers();
|
||||||
|
|
||||||
let req = crate::Request::new(pathname, headers, params);
|
let req = crate::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||||
|
|
||||||
// TODO: remove unwrap
|
// TODO: remove unwrap
|
||||||
let payload = Payload::new(&req, &"").client_payload().unwrap();
|
let payload = Payload::new(&req, &"").client_payload().unwrap();
|
||||||
|
|||||||
@@ -7,12 +7,19 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
const VITE_MANIFEST_PATH: &str = "./out/client/.vite/manifest.json";
|
const VITE_MANIFEST_PATH: &str = "./out/client/.vite/manifest.json";
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct BundleInfo {
|
pub struct BundleInfo {
|
||||||
|
/// TODO: Add also the import field and load the dynamic
|
||||||
|
/// values in the payload bundles.
|
||||||
pub file: String,
|
pub file: String,
|
||||||
|
#[serde(default = "default_css_vector")]
|
||||||
pub css: Vec<String>,
|
pub css: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_css_vector() -> Vec<String> {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Manifest is the mapping between the vite output bundled files
|
/// Manifest is the mapping between the vite output bundled files
|
||||||
/// and the originals.
|
/// and the originals.
|
||||||
/// Vite doc: https://vitejs.dev/config/build-options.html#build-manifest
|
/// Vite doc: https://vitejs.dev/config/build-options.html#build-manifest
|
||||||
@@ -45,6 +52,73 @@ fn remap_manifest_keys(manifest: HashMap<String, BundleInfo>) -> HashMap<String,
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn correctly_parse_the_manifest_json() {
|
||||||
|
let manifest_example = r#"{
|
||||||
|
"../src/routes/index.tsx": {
|
||||||
|
"file": "assets/index.js",
|
||||||
|
"name": "index",
|
||||||
|
"src": "../src/routes/index.tsx",
|
||||||
|
"isDynamicEntry": true,
|
||||||
|
"imports": [
|
||||||
|
"client-main.tsx"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"assets/index.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"meta-tags-lib": {
|
||||||
|
"file": "assets/meta-lib.js",
|
||||||
|
"name": "meta-tags-lib",
|
||||||
|
"imports": [
|
||||||
|
"client-main.tsx"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client-main.tsx": {
|
||||||
|
"file": "assets/client-main.js",
|
||||||
|
"name": "client-main",
|
||||||
|
"src": "client-main.tsx",
|
||||||
|
"isEntry": true,
|
||||||
|
"dynamicImports": [
|
||||||
|
"../src/routes/index.tsx",
|
||||||
|
"../src/routes/pokemons/[pokemon].tsx"
|
||||||
|
],
|
||||||
|
"css": [
|
||||||
|
"assets/client-main.css"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed_manifest =
|
||||||
|
serde_json::from_str::<HashMap<String, BundleInfo>>(manifest_example).unwrap();
|
||||||
|
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
result.insert(
|
||||||
|
"../src/routes/index.tsx".to_string(),
|
||||||
|
BundleInfo {
|
||||||
|
file: "assets/index.js".to_string(),
|
||||||
|
css: vec!["assets/index.css".to_string()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
result.insert(
|
||||||
|
"client-main.tsx".to_string(),
|
||||||
|
BundleInfo {
|
||||||
|
file: "assets/client-main.js".to_string(),
|
||||||
|
css: vec!["assets/client-main.css".to_string()],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
result.insert(
|
||||||
|
"meta-tags-lib".to_string(),
|
||||||
|
BundleInfo {
|
||||||
|
file: "assets/meta-lib.js".to_string(),
|
||||||
|
css: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(parsed_manifest, result);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_remap_the_manifest() {
|
fn should_correctly_remap_the_manifest() {
|
||||||
let mut parsed_manifest: HashMap<String, BundleInfo> = HashMap::new();
|
let mut parsed_manifest: HashMap<String, BundleInfo> = HashMap::new();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub struct Payload<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Payload<'a> {
|
impl<'a> Payload<'a> {
|
||||||
pub fn new(req: &Request<'a>, props: &'a dyn Serialize) -> Payload<'a> {
|
pub fn new(req: &'a Request, props: &'a dyn Serialize) -> Payload<'a> {
|
||||||
Payload {
|
Payload {
|
||||||
router: req.location(),
|
router: req.location(),
|
||||||
props,
|
props,
|
||||||
@@ -135,7 +135,7 @@ mod tests {
|
|||||||
|
|
||||||
use crate::manifest::BundleInfo;
|
use crate::manifest::BundleInfo;
|
||||||
|
|
||||||
fn prepare_payload(uri: Option<&str>, mode: Mode) -> Payload<'static> {
|
fn prepare_payload<'a>(uri: Option<&'a str>, mode: Mode) -> Payload<'a> {
|
||||||
let mut manifest_mock = HashMap::new();
|
let mut manifest_mock = HashMap::new();
|
||||||
manifest_mock.insert(
|
manifest_mock.insert(
|
||||||
"client-main".to_string(),
|
"client-main".to_string(),
|
||||||
@@ -185,11 +185,13 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
MANIFEST.get_or_init(|| manifest_mock);
|
MANIFEST.get_or_init(|| manifest_mock);
|
||||||
let location = Location::from(
|
|
||||||
&uri.unwrap_or("http://localhost:3000/")
|
let uri = uri
|
||||||
.parse::<Uri>()
|
.unwrap_or("http://localhost:3000/")
|
||||||
.unwrap(),
|
.parse::<Uri>()
|
||||||
);
|
.unwrap();
|
||||||
|
|
||||||
|
let location = Location::from(uri);
|
||||||
|
|
||||||
Payload {
|
Payload {
|
||||||
router: location,
|
router: location,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use axum::http::{HeaderMap, Uri};
|
|||||||
pub struct Location {
|
pub struct Location {
|
||||||
href: String,
|
href: String,
|
||||||
pathname: String,
|
pathname: String,
|
||||||
search: HashMap<String, String>,
|
#[serde(rename(serialize = "searchStr"))]
|
||||||
search_str: String,
|
search_str: String,
|
||||||
hash: String,
|
search: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Location {
|
impl Location {
|
||||||
@@ -19,32 +19,28 @@ impl Location {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Uri> for Location {
|
impl From<Uri> for Location {
|
||||||
fn from(uri: &Uri) -> Self {
|
fn from(uri: Uri) -> Self {
|
||||||
|
let query = uri.query().unwrap_or("");
|
||||||
Location {
|
Location {
|
||||||
|
// TODO: build correct href
|
||||||
href: uri.to_string(),
|
href: uri.to_string(),
|
||||||
pathname: uri.path().to_string(),
|
pathname: uri.path().to_string(),
|
||||||
// TODO: handler search map
|
search_str: query.to_string(),
|
||||||
search: HashMap::new(),
|
search: serde_urlencoded::from_str(query).unwrap_or(HashMap::new()),
|
||||||
search_str: uri.query().unwrap_or("").to_string(),
|
|
||||||
hash: "".to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request<'a> {
|
pub struct Request {
|
||||||
uri: &'a Uri,
|
uri: Uri,
|
||||||
pub headers: &'a HeaderMap,
|
pub headers: HeaderMap,
|
||||||
pub params: HashMap<String, String>,
|
pub params: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Request<'a> {
|
impl Request {
|
||||||
pub fn new(
|
pub fn new(uri: Uri, headers: HeaderMap, params: HashMap<String, String>) -> Request {
|
||||||
uri: &'a Uri,
|
|
||||||
headers: &'a HeaderMap,
|
|
||||||
params: HashMap<String, String>,
|
|
||||||
) -> Request<'a> {
|
|
||||||
Request {
|
Request {
|
||||||
uri,
|
uri,
|
||||||
headers,
|
headers,
|
||||||
@@ -53,6 +49,6 @@ impl<'a> Request<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn location(&self) -> Location {
|
pub fn location(&self) -> Location {
|
||||||
Location::from(self.uri)
|
Location::from(self.uri.to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono_lib_macros"
|
name = "tuono_lib_macros"
|
||||||
version = "0.4.3"
|
version = "0.9.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "The react/rust fullstack framework"
|
description = "The react/rust fullstack framework"
|
||||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
State(client): State<tuono_lib::reqwest::Client>,
|
State(client): State<tuono_lib::reqwest::Client>,
|
||||||
request: tuono_lib::axum::extract::Request
|
request: tuono_lib::axum::extract::Request
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let pathname = &request.uri();
|
let pathname = request.uri();
|
||||||
let headers = &request.headers();
|
let headers = request.headers();
|
||||||
|
|
||||||
let req = tuono_lib::Request::new(pathname, headers, params);
|
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(), client).await.render_to_string(req)
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,10 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
State(client): State<tuono_lib::reqwest::Client>,
|
State(client): State<tuono_lib::reqwest::Client>,
|
||||||
request: tuono_lib::axum::extract::Request
|
request: tuono_lib::axum::extract::Request
|
||||||
) -> impl IntoResponse{
|
) -> impl IntoResponse{
|
||||||
let pathname = &request.uri();
|
let pathname = request.uri();
|
||||||
let headers = &request.headers();
|
let headers = request.headers();
|
||||||
|
|
||||||
let req = tuono_lib::Request::new(pathname, headers, params);
|
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||||
|
|
||||||
#fn_name(req.clone(), client).await.json()
|
#fn_name(req.clone(), client).await.json()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
This folder contains the documentation related to the `tuono` framework.
|
||||||
|
|
||||||
|
The framework expects to enable SSG and MDX support in the next months.
|
||||||
|
At that time a standalone website will be built with tuono to hold the whole documentation.
|
||||||
|
|
||||||
|
The following docs are currently actively maintained:
|
||||||
|
- [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
|
||||||
|
|
||||||
|
## Contributions
|
||||||
|
Any help or suggestion will be strongly appreciated.
|
||||||
+95
-8
@@ -23,6 +23,7 @@ Typescript and Rust knowledge is not a requirement though!
|
|||||||
* [Create a stand-alone component](#create-a-stand-alone-component)
|
* [Create a stand-alone component](#create-a-stand-alone-component)
|
||||||
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
|
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
|
||||||
* [Error handling](#error-handling)
|
* [Error handling](#error-handling)
|
||||||
|
* [Seo and meta tags](#seo-and-meta-tags)
|
||||||
* [Handle redirections](#handle-redirections)
|
* [Handle redirections](#handle-redirections)
|
||||||
* [Building for production](#building-for-production)
|
* [Building for production](#building-for-production)
|
||||||
* [Conclusion](#conclusion)
|
* [Conclusion](#conclusion)
|
||||||
@@ -119,6 +120,7 @@ Clear the `index.rs` file and paste:
|
|||||||
```rust
|
```rust
|
||||||
// src/routes/index.rs
|
// src/routes/index.rs
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tuono_lib::reqwest::Client;
|
||||||
use tuono_lib::{Props, Request, Response};
|
use tuono_lib::{Props, Request, Response};
|
||||||
|
|
||||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||||
@@ -135,7 +137,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
|
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||||
return match fetch.get(ALL_POKEMON).send().await {
|
return match fetch.get(ALL_POKEMON).send().await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let data = res.json::<Pokemons>().await.unwrap();
|
let data = res.json::<Pokemons>().await.unwrap();
|
||||||
@@ -328,6 +330,7 @@ Let’s first work on the server side file. Paste into the new `[pokemon].rs` fi
|
|||||||
```rust
|
```rust
|
||||||
// src/routes/pokemons/[pokemon].rs
|
// src/routes/pokemons/[pokemon].rs
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tuono_lib::reqwest::Client;
|
||||||
use tuono_lib::{Props, Request, Response};
|
use tuono_lib::{Props, Request, Response};
|
||||||
|
|
||||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||||
@@ -341,7 +344,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||||
let pokemon = req.params.get("pokemon").unwrap();
|
let pokemon = req.params.get("pokemon").unwrap();
|
||||||
|
|
||||||
@@ -468,8 +471,9 @@ Let's see how it works!
|
|||||||
|
|
||||||
```diff
|
```diff
|
||||||
// src/routes/pokemons/[pokemon].rs
|
// src/routes/pokemons/[pokemon].rs
|
||||||
++ use reqwest::StatusCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
-- use tuono_lib::reqwest::Client;
|
||||||
|
++ use tuono_lib::reqwest::{Client, StatusCode};
|
||||||
use tuono_lib::{Props, Request, Response};
|
use tuono_lib::{Props, Request, Response};
|
||||||
|
|
||||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||||
@@ -483,7 +487,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||||
let pokemon = req.params.get("pokemon").unwrap();
|
let pokemon = req.params.get("pokemon").unwrap();
|
||||||
|
|
||||||
@@ -507,8 +511,9 @@ async fn get_pokemon(req: Request<'_>, fetch: reqwest::Client) -> Response {
|
|||||||
|
|
||||||
```diff
|
```diff
|
||||||
// src/routes/index.rs
|
// src/routes/index.rs
|
||||||
++ use reqwest::StatusCode;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
-- use tuono_lib::reqwest::Client;
|
||||||
|
++ use tuono_lib::reqwest::{Client, StatusCode};
|
||||||
use tuono_lib::{Props, Request, Response};
|
use tuono_lib::{Props, Request, Response};
|
||||||
|
|
||||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||||
@@ -525,7 +530,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response {
|
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||||
return match fetch.get(ALL_POKEMON).send().await {
|
return match fetch.get(ALL_POKEMON).send().await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let data = res.json::<Pokemons>().await.unwrap();
|
let data = res.json::<Pokemons>().await.unwrap();
|
||||||
@@ -543,6 +548,88 @@ async fn get_all_pokemons(_req: Request<'_>, fetch: reqwest::Client) -> Response
|
|||||||
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
|
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
|
||||||
correctly receive a 404 status code in the console.
|
correctly receive a 404 status code in the console.
|
||||||
|
|
||||||
|
## Seo and meta tags
|
||||||
|
|
||||||
|
The website now works and the http errors are meaningful but we should also take care to be meaningful
|
||||||
|
for the web crawlers. The best way to do it is to enrich the meta tags like the `<title>` and the
|
||||||
|
`<description>`.
|
||||||
|
|
||||||
|
To do so `tuono` exposes also the `<Head />` component useful exactly for handling this scenario. Let's update the `/` and the
|
||||||
|
`/pokemons/[pokemon]` routes with this.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
// src/routes/index.tsx
|
||||||
|
import type { TuonoProps } from "tuono";
|
||||||
|
++ import { Head } from "tuono"
|
||||||
|
|
||||||
|
interface Pokemon {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexProps {
|
||||||
|
results: Pokemon[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IndexPage({
|
||||||
|
data,
|
||||||
|
}: TuonoProps<IndexProps>): JSX.Element {
|
||||||
|
if (!data?.results) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
++ <Head>
|
||||||
|
++ <title>Tuono tutorial</title>
|
||||||
|
++ </Head>
|
||||||
|
<header className="header">
|
||||||
|
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||||
|
Crates
|
||||||
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/tuono" target="_blank">
|
||||||
|
Npm
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<div className="title-wrap">
|
||||||
|
<h1 className="title">
|
||||||
|
TU<span>O</span>NO
|
||||||
|
</h1>
|
||||||
|
<div className="logo">
|
||||||
|
<img src="rust.svg" className="rust" />
|
||||||
|
<img src="react.svg" className="react" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
|
||||||
|
{data.results.map((pokemon) => {
|
||||||
|
return pokemon.name;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
// src/routes/pokemons/[pokemon].tsx
|
||||||
|
-- import { TuonoProps } from "tuono";
|
||||||
|
++ import { TuonoProps, Head } from "tuono";
|
||||||
|
import PokemonView from "../../components/PokemonView";
|
||||||
|
|
||||||
|
export default function Pokemon({ data }: TuonoProps): JSX.Element {
|
||||||
|
-- return <PokemonView pokemon={data} />;
|
||||||
|
++ return (
|
||||||
|
++ <>
|
||||||
|
++ <Head>
|
||||||
|
++ <title>Pokemon: ${data?.name}</title>
|
||||||
|
++ </Head>
|
||||||
|
++ <PokemonView pokemon={data} />
|
||||||
|
++ </>
|
||||||
|
++ )
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Head` component takes as children any valid HTML meta tag.
|
||||||
|
|
||||||
## Handle redirections
|
## Handle redirections
|
||||||
|
|
||||||
What if there is a pokemon among all of them that should be considered the GOAT? What
|
What if there is a pokemon among all of them that should be considered the GOAT? What
|
||||||
@@ -553,10 +640,10 @@ First let's create a new route by just creating an new file `/pokemons/GOAT.rs`
|
|||||||
|
|
||||||
```rs
|
```rs
|
||||||
// src/routes/pokemons/GOAT.rs
|
// src/routes/pokemons/GOAT.rs
|
||||||
use tuono_lib::{Request, Response};
|
use tuono_lib::{reqwest::Client, Request, Response};
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn redirect_to_goat(_: Request<'_>, _: reqwest::Client) -> Response {
|
async fn redirect_to_goat(_: Request, _: Client) -> Response {
|
||||||
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
|
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
|
||||||
Response::Redirect("/pokemons/mewtwo".to_string())
|
Response::Redirect("/pokemons/mewtwo".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,13 @@ $ tuono new [NAME] --template [TEMPLATE]
|
|||||||
|
|
||||||
`[TEMPLATE]` is the folder name.
|
`[TEMPLATE]` is the folder name.
|
||||||
|
|
||||||
|
## Troubleshooting for contributors
|
||||||
|
|
||||||
|
There is a common error that happens during example development due to
|
||||||
|
pnpm workspace about the findings of different react versions.
|
||||||
|
|
||||||
|
To overcome this issue run within the single example folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
pnpm install --ignore-workspace && pnpm upgrade --save
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.tuono
|
||||||
|
out
|
||||||
|
target
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "tuono"
|
||||||
|
version = "0.0.1"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "tuono"
|
||||||
|
path = ".tuono/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tuono_lib = { path = "../../crates/tuono_lib/"}
|
||||||
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Tuono starter
|
||||||
|
|
||||||
|
This is the starter tuono project. To download it run in your terminal:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ tuono new [NAME]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "tuono",
|
||||||
|
"description": "The react/rust fullstack framework",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@mdx-js/react": "^3.0.1",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"tuono": "link:../../packages/tuono"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"typescript": "^5.5.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { MDXProvider } from '@mdx-js/react'
|
||||||
|
|
||||||
|
interface RootRouteProps {
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<main className="main">
|
||||||
|
<MDXProvider components={{}}>{children}</MDXProvider>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Index page
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #fbfbfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 673px;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a {
|
||||||
|
color: black;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 200px;
|
||||||
|
line-height: 200px;
|
||||||
|
z-index: 0;
|
||||||
|
letter-spacing: -2px;
|
||||||
|
margin-left: -8px;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 140px;
|
||||||
|
height: 30px;
|
||||||
|
border: solid 3px black;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
color: white;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-left: 240px;
|
||||||
|
position: relative;
|
||||||
|
top: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rust {
|
||||||
|
animation: rotate 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react {
|
||||||
|
top: 33px;
|
||||||
|
left: 28px;
|
||||||
|
animation: rotate 6s linear infinite reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -9,4 +9,5 @@ path = ".tuono/main.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tuono_lib = { path = "../../crates/tuono_lib/"}
|
tuono_lib = { path = "../../crates/tuono_lib/"}
|
||||||
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use tuono_lib::reqwest;
|
||||||
use tuono_lib::{Props, Request, Response};
|
use tuono_lib::{Props, Request, Response};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -7,7 +8,7 @@ struct MyResponse<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_server_side_props(_req: Request<'_>, _fetch: reqwest::Client) -> Response {
|
async fn get_server_side_props(_req: Request, _fetch: reqwest::Client) -> Response {
|
||||||
Response::Props(Props::new(MyResponse {
|
Response::Props(Props::new(MyResponse {
|
||||||
subtitle: "The react / rust fullstack framework",
|
subtitle: "The react / rust fullstack framework",
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ export default function PokemonLink({
|
|||||||
id: number
|
id: number
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
href={`/pokemons/${pokemon.name}`}
|
||||||
|
id={pokemon.name}
|
||||||
|
>
|
||||||
{pokemon.name}
|
{pokemon.name}
|
||||||
<img
|
<img
|
||||||
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
|
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_all_pokemons(_req: Request<'_>, fetch: Client) -> Response {
|
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||||
match fetch.get(ALL_POKEMON).send().await {
|
match fetch.get(ALL_POKEMON).send().await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let data = res.json::<Pokemons>().await.unwrap();
|
let data = res.json::<Pokemons>().await.unwrap();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/routes/index.tsx
|
// src/routes/index.tsx
|
||||||
import type { TuonoProps } from 'tuono'
|
import { Head, type TuonoProps } from 'tuono'
|
||||||
import PokemonLink from '../components/PokemonLink'
|
import PokemonLink from '../components/PokemonLink'
|
||||||
|
|
||||||
interface Pokemon {
|
interface Pokemon {
|
||||||
@@ -19,6 +19,9 @@ export default function IndexPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Tuono tutorial</title>
|
||||||
|
</Head>
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<a href="https://crates.io/crates/tuono" target="_blank">
|
<a href="https://crates.io/crates/tuono" target="_blank">
|
||||||
Crates
|
Crates
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
use tuono_lib::{reqwest::Client, Request, Response};
|
use tuono_lib::{reqwest::Client, Request, Response};
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn redirect_to_goat(_: Request<'_>, _: Client) -> Response {
|
async fn redirect_to_goat(_: Request, _: Client) -> Response {
|
||||||
Response::Redirect("/pokemons/mewtwo".to_string())
|
Response::Redirect("/pokemons/mewtwo".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct Pokemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tuono_lib::handler]
|
#[tuono_lib::handler]
|
||||||
async fn get_pokemon(req: Request<'_>, fetch: Client) -> Response {
|
async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||||
// The param `pokemon` is defined in the route filename [pokemon].rs
|
// The param `pokemon` is defined in the route filename [pokemon].rs
|
||||||
let pokemon = req.params.get("pokemon").unwrap();
|
let pokemon = req.params.get("pokemon").unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { TuonoProps } from 'tuono'
|
import { Head, type TuonoProps } from 'tuono'
|
||||||
import PokemonView from '../../components/PokemonView'
|
import PokemonView from '../../components/PokemonView'
|
||||||
|
|
||||||
interface Pokemon {
|
interface Pokemon {
|
||||||
@@ -11,5 +11,12 @@ interface Pokemon {
|
|||||||
export default function PokemonPage({
|
export default function PokemonPage({
|
||||||
data,
|
data,
|
||||||
}: TuonoProps<Pokemon>): JSX.Element {
|
}: TuonoProps<Pokemon>): JSX.Element {
|
||||||
return <PokemonView pokemon={data} />
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Pokemon: {data?.name}</title>
|
||||||
|
</Head>
|
||||||
|
<PokemonView pokemon={data} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -2,18 +2,20 @@
|
|||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
"packageManager": "pnpm@9.1.1",
|
"packageManager": "pnpm@9.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo dev --filter tuono",
|
"dev": "turbo watch dev",
|
||||||
"build": "turbo build",
|
"build": "turbo build --filter tuono",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"format": "turbo format",
|
"format": "turbo format",
|
||||||
"format:check": "turbo format:check",
|
"format:check": "turbo format:check",
|
||||||
"types": "turbo types",
|
"types": "turbo types --filter tuono",
|
||||||
"test": "turbo test",
|
"test": "turbo test",
|
||||||
"test:watch": "turbo test:watch"
|
"test:watch": "turbo test:watch"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"tuono",
|
"tuono",
|
||||||
"tuono-lazy-fn-vite-plugin"
|
"tuono-lazy-fn-vite-plugin",
|
||||||
|
"tuono-fs-router-vite-plugin",
|
||||||
|
"tuono-router"
|
||||||
],
|
],
|
||||||
"author": "Valerio Ageno",
|
"author": "Valerio Ageno",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -36,6 +38,6 @@
|
|||||||
"vitest": "^1.5.2"
|
"vitest": "^1.5.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"turbo": "^2.0.4"
|
"turbo": "^2.0.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# tuono-fs-router-vite-plugin
|
||||||
|
|
||||||
|
This is a vite plugin for [tuono](https://github.com/Valerioageno/tuono).
|
||||||
|
|
||||||
|
This package specifically handles the file system based routing.
|
||||||
|
|
||||||
|
Check [tuono](https://github.com/Valerioageno/tuono) for more.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This plugin is strongly inspired by the [@tanstack/router](https://tanstack.com/router/latest)
|
||||||
|
route generator plugin.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user