Compare commits

...

46 Commits

Author SHA1 Message Date
Valerio Ageno 1f92931076 Fix optional application state parameters (#173)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-27 15:25:05 +01:00
renovate[bot] d3105a1532 fix(deps): update dependency turbo to v2.3.3 (#169) 2024-11-27 08:26:54 +01:00
Marco Pasqualetti 7f50f31837 ci: trigger workflow only on push only on main (#166) 2024-11-26 21:49:14 +01:00
renovate[bot] 4c3e90ea4b chore(deps): update dependency @vitejs/plugin-react to v4.3.4 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:57 +01:00
renovate[bot] 8d80fc4aa9 chore(deps): update dependency vitest to v2.1.6 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:38 +01:00
renovate[bot] 78a04ffcf1 chore(deps): update dependency prettier to v3.4.1 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:23 +01:00
Valerio Ageno bcb2c91823 fix: windows path resolution (#167) 2024-11-26 21:37:00 +01:00
Marco Pasqualetti 9058f209dd ci: add Windows and MacOS to test matrix for both rust and typescript (#158) 2024-11-26 17:50:53 +01:00
renovate[bot] 1f172d583e fix(deps): update dependency @vitejs/plugin-react-swc to v3.7.2 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 13:48:13 +01:00
Valerio Ageno 439b4757df Turn main.rs into app.rs (#159)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-26 09:20:10 +01:00
renovate[bot] 9f8809bd51 fix(deps): update dependency @types/node to v22.10.0 (#157) 2024-11-26 08:02:27 +01:00
renovate[bot] 4e326650cc fix(deps): update dependency @types/node to v22.10.0 (#154) 2024-11-26 07:59:15 +01:00
renovate[bot] e27be27692 fix(deps): update dependency turbo to v2.3.2 (#155) 2024-11-26 07:58:58 +01:00
renovate[bot] 88eb2a1897 chore(deps): update dependency prettier to v3.4.0 (#156) 2024-11-26 07:58:43 +01:00
Valerio Ageno 8c84ef73c8 feat: update version to v0.14.1 2024-11-24 18:33:45 +01:00
renovate[bot] 859e49f16c fix(deps): update mantine monorepo to v7.14.2 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 18:16:13 +01:00
renovate[bot] 14ba8b0a23 chore(deps): update dependency typescript to v5.7.2 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-23 21:28:03 +01:00
Marco Pasqualetti d7c70f75f3 docs(CONTRIBUTING): add “Local development” (#136) 2024-11-23 21:04:01 +01:00
Valerio Ageno 2760e3ea08 feat(ci): update spellchecker workflow (#143) 2024-11-23 17:30:05 +01:00
Marco Pasqualetti f2f041fe55 ci: test spellcheck via reviewdog (#137)
* ci: use GITHUB_TOKEN in spell checking workflow

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

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

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

* feat: create basic API handler proc macro

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

* chore: remove unused cargo dependencies

* refactor: tuono base health_check

* fix: remove failing test

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

* feat: expand port availability checking

* refactor: improve port extraction code modularity

* fix: formatting

* fix: clippy

* fix: move port checking logic to tuono crate

* fix(tuono): make main async

* fix: tokio panicking when testing CLI

* feat: working port conflict detection in crate tuono

* fix: passing format and lint checks

* refactor: refactor cli by making it non-async

* refactor: move port constants to module scope and remove unnecessary Result return type from port checker
2024-11-21 09:45:08 +01:00
Valerio Ageno eaa0ba28d8 Update README.md 2024-11-20 18:57:43 +01:00
renovate[bot] 3f3e1120b2 chore(deps): update pnpm to v9.14.2 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:52:19 +01:00
Marco Pasqualetti 89d7a2c740 chore: add examples folder to workspaces and make examples packages name unique (#125) 2024-11-20 13:46:31 +01:00
Valerio Ageno 56c31f0204 feat: update version to v0.13.3 2024-11-20 13:45:37 +01:00
renovate[bot] 338cfb5be2 fix(deps): update dependency @types/node to v22.9.1 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:32 +01:00
renovate[bot] 43f6fe664b chore(deps): update pnpm to v9.14.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:03 +01:00
Valerio Ageno 209c9fb7f7 feat: update version to v0.13.2 2024-11-19 17:34:33 +01:00
Valerio Ageno a562f67e8d Feat: added discord to README 2024-11-19 16:43:58 +01:00
Valerio Ageno ef0fe5a425 fix: seo page canonical 2024-11-19 15:50:58 +01:00
Myan V. b50902d23e Update watchexec to v5 and watchexec-signals to v4 (#118)
* fix(deps): update rust crate watchexec to v5

* fix(deps): update rust crate watchexec-signals to v4

* fix: revert unnecessary imports
2024-11-19 14:32:44 +01:00
Marco Pasqualetti 55490579ec refactor: replace () => JSX.Element with React.ComponentType (#120) 2024-11-18 21:45:27 +01:00
renovate[bot] 957d08701f chore(deps): update typescript-eslint monorepo to v8.15.0 (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 20:41:04 +01:00
Valerio Ageno 61e1aed35d Add not expiring discord invite to documentation website 2024-11-18 18:36:33 +01:00
Valerio Ageno 2034f0ef5f fix: tutorial/redirections canonical tag 2024-11-18 12:35:14 +01:00
Valerio Ageno fcb92c7f0b fix(doc): spelling 2024-11-17 18:24:56 +01:00
Marco Pasqualetti 28ea1ca1df ci: add format check on repository root (#115) 2024-11-17 17:51:20 +01:00
Valerio Ageno ebc1a458a9 Fix tuono_lib_macro crate (#116)
* fix: tuono lib macro

* feat: remove reqwest re-export

* feat: update version to 0.13.1

* fix: remove reqwest from documentation
2024-11-17 17:11:07 +01:00
103 changed files with 1385 additions and 565 deletions
+14 -14
View File
@@ -34,51 +34,51 @@ body:
required: false
- type: input
id: tuono-version
attributes:
attributes:
label: Tuono version
placeholder: "[e.g. 0.4.0]"
placeholder: '[e.g. 0.4.0]'
validations:
required: false
- type: input
id: os-version
attributes:
attributes:
label: OS
placeholder: "[e.g. MacOS, Windows]"
placeholder: '[e.g. MacOS, Windows]'
validations:
required: false
- type: input
id: browser-version
attributes:
attributes:
label: Browser
placeholder: "[e.g. chrome, safari]"
placeholder: '[e.g. chrome, safari]'
validations:
required: false
- type: input
id: node-version
attributes:
attributes:
label: Node version
placeholder: "[e.g. 20.0.0]"
placeholder: '[e.g. 20.0.0]'
validations:
required: false
- type: input
id: rust-version
attributes:
attributes:
label: Rust version
placeholder: "[e.g. 1.79.0]"
placeholder: '[e.g. 1.79.0]'
validations:
required: false
- type: input
id: create-version
attributes:
attributes:
label: Crate version
placeholder: "[e.g. 1.78.0]"
placeholder: '[e.g. 1.78.0]'
validations:
required: false
- type: input
id: node-package-manager-version
attributes:
attributes:
label: Node Package Manger version
placeholder: "[e.g. pnpm: 9.5.0] "
placeholder: '[e.g. pnpm: 9.5.0] '
validations:
required: false
- type: textarea
@@ -33,4 +33,3 @@ body:
description: Add any other context or screenshots about the feature request here.
validations:
required: false
+1 -1
View File
@@ -1,7 +1,7 @@
## Context & Description
<!--
Thank you for your Pull Request.
Thank you for your Pull Request.
Explain the context and why you're making that change. What is the problem
you're trying to solve? If a new feature is being added, describe the intended
+5 -5
View File
@@ -2,6 +2,8 @@ name: Documentation Website CI
on:
push:
branches:
- main
paths:
- '.github/**'
- 'apps/documentation/**'
@@ -12,10 +14,9 @@ on:
jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check build
timeout-minutes: 15
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
@@ -34,9 +35,8 @@ jobs:
working-directory: ./apps/documentation
run: tuono build --static
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check format and lint
code_check:
name: Check format, lint and types
runs-on: ubuntu-latest
timeout-minutes: 15
@@ -1,4 +1,5 @@
name: Deploy documentation website on AWS S3
on:
push:
branches:
@@ -9,6 +10,7 @@ on:
jobs:
deploy-documentation:
name: Deploy
runs-on: ubuntu-latest
env:
+15 -6
View File
@@ -1,11 +1,14 @@
name: Publish on npm and crates
on:
release:
types: [published]
jobs:
publish-on-npm-dry-run:
publish_npm_dry_run:
name: NPM publish (dry run)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -23,8 +26,10 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-on-crates-dry-run:
publish_crates_dry_run:
name: Crates publish (dry run)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -41,9 +46,11 @@ jobs:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
check-repo: true
publish-on-npm:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
publish_npm:
name: NPM publish
needs: [publish_npm_dry_run, publish_crates_dry_run]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -64,9 +71,11 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-on-crates:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
publish_crates:
name: Crates publish
needs: [publish_npm_dry_run, publish_crates_dry_run]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
+32
View File
@@ -0,0 +1,32 @@
name: Repo root CI
# All workflows are triggered based on specific paths,
# the root repository is something that is not applicable to rust nor typescript
# so it has been moved in a standalone workflow
on:
push:
branches:
- main
paths:
- '.github/**'
- './*'
pull_request:
paths:
- '.github/**'
- './*'
jobs:
build-and-test:
name: Check format
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Test project
run: pnpm repo:root:format:check
+27 -19
View File
@@ -2,6 +2,8 @@ name: Rust CI
on:
push:
branches:
- main
paths:
- '.github/**'
- 'crates/**'
@@ -14,8 +16,32 @@ env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Build and test rust crates
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
toolchain:
- stable
- beta
- nightly
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
lint_and_fmt:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Check format and lint rust crates
runs-on: ubuntu-latest
@@ -25,21 +51,3 @@ jobs:
- run: cargo fmt --all -- --check
- run: cargo clippy -- -D warnings
build_and_test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Build and test rust crates
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
+51 -8
View File
@@ -1,23 +1,66 @@
name: Spell Checker
on: [pull_request]
on:
push:
branches:
- main
pull_request:
jobs:
spell_checking:
name: runner / LanguageTools
name: LanguageTool
runs-on: ubuntu-latest
permissions:
checks: write
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
# Keep this value in order to show the error comments
# also on edited files.
fetch-depth: 2
# If the workflow is triggered by a push use check reporter
#
# If the workflow is triggered by a PR coming from the
# same repository, the author has write access so we can
# use pr-review reporter since the GITHUB_TOKEN will have review
# write permissions
#
# Otherwise use annotation reporter that do not require any
# particular permission
#
# Why this?
# https://github.com/Valerioageno/tuono/pull/137#pullrequestreview-2455455272
- name: Set reporter
id: reporter
env:
EVENT_NAME: ${{ github.event_name }}
FROM_MAIN_REPO: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
run: |
echo "EVENT_NAME: $EVENT_NAME FROM_MAIN_REPO: $FROM_MAIN_REPO"
if [ "$EVENT_NAME" == 'push' ]; then
echo "name=github-check" >> $GITHUB_OUTPUT
elif [ "$FROM_MAIN_REPO" == 'true' ]; then
echo "name=github-pr-review" >> $GITHUB_OUTPUT
else
echo "name=github-pr-annotations" >> $GITHUB_OUTPUT
fi
- name: Review dog languagetool
uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
reporter: 'github-pr-review'
reporter: ${{ steps.reporter.outputs.name }}
# Change reporter level if you need.
level: 'error'
level: 'warning'
patterns: '**/*.md **/*.txt **/*.mdx'
#enabled_categories: 'STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END'
# enabled_categories: "STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END"
disabled_categories: ''
disabled_rules: ''
+14 -5
View File
@@ -2,6 +2,8 @@ name: Typescript CI
on:
push:
branches:
- main
paths:
- '.github/**'
- 'packages/**'
@@ -11,11 +13,19 @@ on:
- 'packages/**'
jobs:
build-and-test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
build_and_test:
name: Check build and test
timeout-minutes: 15
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
@@ -30,8 +40,7 @@ jobs:
- name: Test project
run: pnpm test
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
lint_and_fmt:
name: Check format and lint
runs-on: ubuntu-latest
timeout-minutes: 15
+1
View File
@@ -0,0 +1 @@
pnpm-lock.yaml
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"trailingComma": "all"
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best, not just for us as individuals, but for the
- Focusing on what is best, not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
Advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct that could reasonably be considered inappropriate in a
- Other conduct that could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+4 -1
View File
@@ -4,7 +4,8 @@
# How to Contribute to Tuono
## Contributions
## Contributions
Any feature contribution or suggestion is strongly appreciated.
Since the current project size, there isn't yet a defined way to start a discussion. Consider [opening a new issue](https://github.com/Valerioageno/tuono/issues/new/choose)
or to reach me using my email address, [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on Twitter (X); DM @valerioageno.
@@ -12,10 +13,12 @@ or to reach me using my email address, [valerioageno@yahoo.it](mailto:valerioage
## Bugs
**Did you find a bug?**
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Valerioageno/tuono/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
**Did you write a patch that fixes a bug?**
- Open a new GitHub pull request with the patch.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number, if applicable.
- The pull requests must pass all the CI pipelines
+3 -3
View File
@@ -7,8 +7,8 @@ members = [
]
exclude = [
"apps/documentation",
"examples/mdx",
"examples/tuono",
"examples/tutorial",
"examples/wit-mdx",
"examples/tuono-app",
"examples/tuono-tutorial",
"benches/tuono"
]
+4 -6
View File
@@ -1,5 +1,5 @@
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
[✨Contributing](https://tuono.dev/documentation/contributing)
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
# Tuono
@@ -8,7 +8,8 @@
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
**Tuono is the fastest React framework out there**
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
a strong focus on usability and performance.**
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
@@ -51,9 +52,6 @@ Tuono runs the server without any intermediary runtime. This enables impressive
## Getting started
As already mentioned above, I strongly suggest you take a look at the
[tutorial](https://tuono.dev/documentation/tutorial).
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
package manager.
@@ -65,7 +63,7 @@ To list all the available commands, run `tuono -h`
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
Then, to run the local development environment, run it inside the project folder `tuono dev`
Then, to run the local development environment, install the JS dependencies (`npm install`) and run inside the project folder `tuono dev`
Finally, when the project is ready to be deployed, just run `tuono build` to create the final React assets
and to set the server project in `production` mode.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 42 KiB

@@ -37,7 +37,7 @@ export default function Actions(): JSX.Element {
variant="default"
size="lg"
aria-label="Join the Tuono's community on Discord!"
href="https://discord.gg/BUvWmmRARx"
href="https://discord.com/invite/khQzPa654B"
target="_blank"
component="a"
>
@@ -3,7 +3,11 @@ import { AppShell } from '@mantine/core'
import SidebarLink from './sidebar-link'
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
interface SidebarProps {
close: () => void
}
export default function Sidebar({ close }: SidebarProps): JSX.Element {
return (
<AppShell.Navbar p="md">
<SidebarLink
@@ -86,12 +90,19 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
onClick={close}
/>
</SidebarLink>
<SidebarLink
label="Contributing"
href="/documentation/contributing"
onClick={close}
leftSection="✨"
/>
onClick={close}
>
<SidebarLink
href="/documentation/contributing/local-development"
label="Local development"
onClick={close}
/>
</SidebarLink>
</AppShell.Navbar>
)
}
@@ -12,13 +12,13 @@ import Breadcrumbs, { Element } from '../../components/breadcrumbs'
# Application state
The main reason Tuono is fast it's because it loads just the features that are needed for the project.
The main reason Tuono is fast is that it loads just the features that are needed for the project.
To defined them you need to fill the `ApplicationState` struct in the `./src/main.rs` file and they will be automatically available
in all the handlers you will define across the application.
To define them, you need to fill the `ApplicationState` struct in the `./src/app.rs` file, and
they will be automatically available in all the handlers you will define across the application.
```rs
// src/main.rs
// src/app.rs
#[derive(Clone)]
pub struct ApplicationState {
@@ -0,0 +1,57 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Contributing"
canonical="https://tuono.dev/documentation/contributing"
description="The project is massive - if you like it, do consider contributing!"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
# Contributing
## TL;DR
The project is massive - if you like it, do consider contributing!
## Getting started
The `tuono` project can mostly be split into the following subdomains:
- The CLI
- The Rust backend
- The React frontend
- The documentation website (which is written with tuono 🚀)
To check what the knowledge requirements are for each domain, check the
[requirements](#requirements) section below.
Currently, I'm keeping a private dashboard to prioritize new features and bug fixes, but if you
want to propose something, please open a new issue on GitHub or reach out to me using
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
on Twitter (X) DMs `@valerioageno`, [Linkedin](https://www.linkedin.com/in/valerioageno)
and discord `@__v__v__`.
## Requirements
It's not strictly required to know both React (& typescript) and Rust (even though it
would be a great nice to have).
Without taking into account specific cases, we can mostly split the domain requirements by:
- The `CLI` needs Rust knowledge (even though a couple of scenarios might also need Typescript)
- The Backend needs just `Rust`
- The Frontend needs just `React` & `Typescript`
- The documentation website needs just `React` & `Typescript` (or even less, since most of the
code is markdown).
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
next={{
title: 'Local development',
href: '/documentation/contributing/local-development',
}}
/>
@@ -0,0 +1,113 @@
import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Contributing - Local development"
canonical="https://tuono.dev/documentation/contributing/local-development"
description="Contribute to Tuono. Learn here how to setup the repository for local development"
/>
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
<Breadcrumbs
breadcrumbs={[
{ label: '✨ Contributing', href: '/documentation/contributing' },
{ label: 'Local development' },
]}
/>
# Local development
Thanks for your interest! In this page, you find the instructions to set up `tuono` on your local environment!
## Setup
### Fork and clone repository
After [forking the repo on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo):
```sh
git clone https://github.com/<your-name-here>/tuono
cd tuono
```
### Rust tool chain
Install the Rust programming language tool chain (`rust` and `cargo`).
Follows instructions in the official [docs](https://rustup.rs/)
### Node.js — runtime
Install `Node.js`.
You can follow the instructions from the [Node official site](https://nodejs.org/en/download/package-manager)
> 💡 This project has a `.nvmrc` file to specify the node version used in development.
>
> Consider to use [nvm](https://github.com/nvm-sh/nvm) so you can run
>
> ```sh
> nvm use
> ```
>
> to simply pick up the correct version!
### Node.js — package manager
We use [`pnpm`](https://pnpm.io) as Node.js package manager.
You can see which version of yarn we use by checking the `packageManager` field in the root `package.json`.
### Pre-flight checks
To check that everything is working properly, run:
```sh
turbo run check-all
cargo build
```
## Tuono development
1. Start tuono frontend build using
```sh
turbo run dev
```
2. In another terminal run
```sh
cargo build
```
To automatically rebuild crates on code change, consider using `cargo-watch` crate
```sh
cargo watch -x build -w crates/
```
3. You can now use the binary inside `/target/debug/tuono` in another folder on your local machine
> Consider adding an alias to your shell setup file
>
> ```sh
> alias t="/path-to-repo/target/debug/tuono"
> ```
## Documentation development
1. Change the current working directory to the documentation folder:
```sh
cd apps/documentation
```
2. Run
```sh
tuono dev
```
3. Open the localhost URL.
> On the documentation remember that `tuono` `npm` package is installed from the registry and
> it is not linked to the repository.
@@ -68,9 +68,9 @@ Let's fix these in the next section.
## Application state
Compared to the common Javascript runtimes Tuono is fast because only the features you need for your project will be loaded.
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application.
You can load them in the `ApplicationState` of your app inside the `./src/app.rs` file. This is the file that will be executed just once at the very beginning of your application.
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
@@ -98,9 +98,10 @@ serde = { version = "1.0.202", features = ["derive"] }
++ reqwest = "0.12.9" # the version might be different
```
> The `Cargo.toml` is the manifest file of your application in which handling the Rust's dependencies (similarly as the `package.json` for Javascript).
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
> (similarly as the package.json for Javascript).
Now let's define the `ApplicationState` in the `./src/main.rs` file.
Now let's define the `ApplicationState` in the `./src/app.rs` file.
```rs
// Import here the just added reqwest library
@@ -119,7 +120,7 @@ pub fn main() -> ApplicationState {
}
```
Now the `fetch: Client` argument is available in the above defined handler and the terminal should not complain anymore.
Now the `fetch: Client` argument is available in the above defined handler, and the terminal should not complain anymore.
Let's see in the next section how to show the fetched data on the browser.
## Handling the page UI
@@ -32,7 +32,7 @@ Lets first work on the server side file. Paste into the new `[pokemon].rs` fi
```rust
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use reqwest::Client;
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -27,8 +27,8 @@ Let's see how it works!
```diff
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
-- use reqwest::Client;
++ use reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
@@ -69,8 +69,8 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
```diff
// src/routes/index.rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
-- use reqwest::Client;
++ use reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
@@ -21,7 +21,7 @@ This tutorial is not meant for people who don't know React - in that case, I sug
If you prefer to just read the code rather than write you can download the finished tutorial project with:
```bash
$ tuono new tutorial --template tutorial
$ tuono new tutorial --template tuono-tutorial
```
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it)
@@ -2,7 +2,7 @@ import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - Redirection"
canonical="https://tuono.dev/documentation/tutorial/redirection"
canonical="https://tuono.dev/documentation/tutorial/redirections"
description="Learn how to redirect to different route from the server"
/>
@@ -2,7 +2,7 @@ import MetaTags from '../../../components/meta-tags'
<MetaTags
title="Tuono - SEO and meta tags"
canonical="https://tuono.dev/documentation/tutorial/eeo"
canonical="https://tuono.dev/documentation/tutorial/seo"
description="Learn how to handle meaningful meta tags for your Tuono project"
/>
+4 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.13.0"
version = "0.14.4"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
@@ -21,9 +21,9 @@ path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive", "cargo"] }
watchexec = "4.0.0"
watchexec = "5.0.0"
miette = "7.2.0"
watchexec-signals = "3.0.0"
watchexec-signals = "4.0.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
watchexec-supervisor = "3.0.0"
@@ -32,3 +32,4 @@ regex = "1.10.4"
reqwest = {version = "0.12.4", features =["blocking", "json"]}
serde_json = "1.0"
fs_extra = "1.3.0"
http = "1.1.0"
+136 -26
View File
@@ -1,5 +1,7 @@
use glob::glob;
use glob::GlobError;
use http::Method;
use std::collections::hash_set::HashSet;
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io::prelude::*;
@@ -14,15 +16,25 @@ use crate::route::Route;
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
const IGNORE_FILES: [&str; 1] = ["__root"];
#[cfg(target_os = "windows")]
const ROUTES_FOLDER_PATH: &str = "\\src\\routes";
#[cfg(target_os = "windows")]
const BUILD_JS_SCRIPT: &str = ".\\node_modules\\.bin\\tuono-build-prod.cmd";
#[cfg(not(target_os = "windows"))]
const ROUTES_FOLDER_PATH: &str = "/src/routes";
#[cfg(not(target_os = "windows"))]
const BUILD_JS_SCRIPT: &str = "./node_modules/.bin/tuono-build-prod";
#[derive(Debug)]
pub struct App {
pub route_map: HashMap<String, Route>,
pub base_path: PathBuf,
pub has_main_file: bool,
pub has_app_state: bool,
}
fn has_main_file(base_path: PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/main.rs"))?;
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/app.rs"))?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
@@ -31,12 +43,12 @@ fn has_main_file(base_path: PathBuf) -> std::io::Result<bool> {
impl App {
pub fn new() -> Self {
let base_path = std::env::current_dir().unwrap();
let base_path = std::env::current_dir().expect("Failed to read current_dir");
let mut app = App {
route_map: HashMap::new(),
base_path: base_path.clone(),
has_main_file: has_main_file(base_path).unwrap_or(false),
has_app_state: has_app_state(base_path).unwrap_or(false),
};
app.collect_routes();
@@ -45,18 +57,31 @@ impl 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)
}
})
glob(
self.base_path
.join("src/routes/**/*.*")
.to_str()
.expect("Failed to glob routes folder"),
)
.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();
let file_extension = entry
.as_ref()
.unwrap()
.extension()
.expect("Failed to read file extension");
let file_name = entry
.as_ref()
.unwrap()
.file_stem()
.expect("Failed to read file name");
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
return false;
@@ -69,17 +94,23 @@ impl App {
}
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 entry = path_buf.expect("Failed to read glob path");
let base_path_str = self
.base_path
.to_str()
.expect("Failed to read as str base_path");
let path = entry
.to_str()
.unwrap()
.replace(&format!("{base_path_str}/src/routes"), "")
.expect("Failed to read entry as str")
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
// Cleanup windows paths
.replace("\\", "/")
.replace(".rs", "")
.replace(".mdx", "")
.replace(".tsx", "");
if entry.extension().unwrap() == "rs" {
if entry.extension().expect("failed to read entry extension") == "rs" {
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
let mut route = Route::new(path);
route.update_axum_info();
@@ -101,7 +132,7 @@ impl App {
}
pub fn build_react_prod(&self) {
Command::new("./node_modules/.bin/tuono-build-prod")
Command::new(BUILD_JS_SCRIPT)
.output()
.expect("Failed to build the react source");
}
@@ -115,6 +146,24 @@ impl App {
.spawn()
.expect("Failed to run the rust server")
}
pub fn get_used_http_methods(&self) -> HashSet<Method> {
let mut acc = HashSet::new();
for (_, route) in self.route_map.clone().into_iter() {
if route.axum_info.is_some() {
acc.insert(Method::GET);
}
if !route.is_api() {
continue;
}
for method in route.api_data.unwrap().methods.into_iter() {
acc.insert(method);
}
}
acc
}
}
#[cfg(test)]
@@ -125,8 +174,27 @@ mod tests {
#[test]
fn should_collect_routes() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
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\\handle-this.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this\\[post].rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\UPPERCASE.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\sitemap.xml.rs",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
@@ -173,8 +241,25 @@ mod tests {
#[test]
fn should_create_multi_level_axum_paths() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
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",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
@@ -199,7 +284,7 @@ mod tests {
assert_eq!(
app.route_map
.get(path)
.unwrap()
.expect("Failed to get route path")
.axum_info
.as_ref()
.unwrap()
@@ -251,8 +336,23 @@ mod tests {
#[test]
fn should_correctly_parse_routes_with_server_handler() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
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",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/about.tsx",
@@ -269,9 +369,19 @@ mod tests {
.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())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_some())
} else {
assert!(app.route_map.get(path).unwrap().axum_info.is_none())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_none())
}
})
}
+34
View File
@@ -11,6 +11,9 @@ use crate::scaffold_project;
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
use crate::watch;
const TUONO_PORT: u16 = 3000;
const VITE_PORT: u16 = 3001;
#[derive(Subcommand, Debug)]
enum Actions {
/// Start the development environment
@@ -47,12 +50,41 @@ fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
Ok(())
}
fn check_ports(mode: Mode) {
let rust_listener = std::net::TcpListener::bind(format!("0.0.0.0:{TUONO_PORT}"));
if let Err(_e) = rust_listener {
eprintln!("Error: Failed to bind to port {}", TUONO_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
TUONO_PORT
);
std::process::exit(1);
}
if mode == Mode::Dev {
let vite_listener = std::net::TcpListener::bind(format!("0.0.0.0:{VITE_PORT}"));
if let Err(_e) = vite_listener {
eprintln!("Error: Failed to bind to port {}", VITE_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
VITE_PORT
);
std::process::exit(1);
}
}
}
pub fn app() -> std::io::Result<()> {
let args = Args::parse();
match args.action {
Actions::Dev => {
check_ports(Mode::Dev);
init_tuono_folder(Mode::Dev)?;
watch::watch().unwrap();
}
Actions::Build { ssg } => {
@@ -68,6 +100,8 @@ pub fn app() -> std::io::Result<()> {
app.build_react_prod();
if ssg {
check_ports(Mode::Prod);
println!("SSG: generation started");
let static_dir = PathBuf::from("out/static");
+54 -2
View File
@@ -1,17 +1,19 @@
use fs_extra::dir::create_all;
use http::Method;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AxumInfo {
// Path for importing the module
pub module_import: String,
@@ -65,11 +67,56 @@ impl AxumInfo {
// TODO: to be extended with common scenarios
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
#[derive(Debug, PartialEq, Eq)]
// TODO: Refine this function to catch
// if the methods are commented.
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
regex
.find_iter(&file)
.map(|proc_macro| {
let http_method = proc_macro
.as_str()
// Extract just the element surrounded by the phrantesist.
.replace("tuono_lib::api(", "")
.replace(")]", "");
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
})
.collect::<Vec<Method>>()
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ApiData {
pub methods: Vec<Method>,
}
impl ApiData {
pub fn new(path: &String) -> Option<Self> {
if !path.starts_with("/api/") {
return None;
}
let base_path = std::env::current_dir().expect("Failed to get the base_path");
let file_path = base_path
.join(format!("src/routes{path}.rs"))
.to_str()
.unwrap()
.to_string();
let methods = read_http_methods_from_file(&file_path);
Some(ApiData { methods })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
path: String,
pub is_dynamic: bool,
pub axum_info: Option<AxumInfo>,
pub api_data: Option<ApiData>,
}
impl Route {
@@ -78,9 +125,14 @@ impl Route {
path: cleaned_path.clone(),
axum_info: None,
is_dynamic: has_dynamic_path(&cleaned_path),
api_data: ApiData::new(&cleaned_path),
}
}
pub fn is_api(&self) -> bool {
self.api_data.is_some()
}
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self.path.clone()))
}
+9 -3
View File
@@ -42,7 +42,7 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
let folder = folder_name.unwrap_or(".".to_string());
// In case of missing select the tuono example
let template = template.unwrap_or("tuono".to_string());
let template = template.unwrap_or("tuono-app".to_string());
let client = blocking::Client::builder()
.user_agent("")
@@ -63,11 +63,17 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
.collect::<Vec<&GithubFile>>();
if new_project_files.is_empty() {
println!("Template not found: {template}");
return;
eprintln!("Error: Template '{template}' not found");
println!("Hint: you can view the available templates at https://github.com/Valerioageno/tuono/tree/main/examples");
std::process::exit(1);
}
if folder != "." {
if Path::new(&folder).exists() {
eprintln!("Error: Directory '{folder}' already exists");
println!("Hint: you can scaffold a tuono project within an existing folder with 'cd {folder} && tuono new .'");
std::process::exit(1);
}
create_dir(&folder).unwrap();
}
+29 -23
View File
@@ -82,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
module_import,
} = axum_info.as_ref().unwrap();
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
));
}
}
}
}
@@ -139,8 +149,8 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
.replace("/*MODE*/", mode.as_str())
.replace(
"//MAIN_FILE_IMPORT//",
if app.has_main_file {
r#"#[path="../src/main.rs"]
if app.has_app_state {
r#"#[path="../src/app.rs"]
mod tuono_main_state;
"#
} else {
@@ -149,7 +159,7 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
)
.replace(
"//MAIN_FILE_DEFINITION//",
if app.has_main_file {
if app.has_app_state {
"let user_custom_state = tuono_main_state::main();"
} else {
""
@@ -157,27 +167,23 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
)
.replace(
"//MAIN_FILE_USAGE//",
if app.has_main_file {
if app.has_app_state {
".with_state(user_custom_state)"
} else {
""
},
);
let has_server_handlers = app
.route_map
.iter()
.filter(|(_, route)| route.axum_info.is_some())
.collect::<Vec<(&String, &Route)>>()
.is_empty();
let mut import_http_handler = String::new();
if !has_server_handlers {
return src.replace(
"// AXUM_GET_ROUTE_HANDLER",
"use tuono_lib::axum::routing::get;",
);
let used_http_methods = app.get_used_http_methods();
for method in used_http_methods.into_iter() {
let method = method.to_string().to_lowercase();
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
}
src
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
}
pub fn check_tuono_folder() -> io::Result<()> {
+13 -3
View File
@@ -9,10 +9,20 @@ use watchexec_supervisor::job::{start_job, Job};
use crate::mode::Mode;
use crate::source_builder::bundle_axum_source;
#[cfg(target_os = "windows")]
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
#[cfg(target_os = "windows")]
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
#[cfg(not(target_os = "windows"))]
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
#[cfg(not(target_os = "windows"))]
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
fn watch_react_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-watch".into(),
prog: DEV_WATCH_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -34,7 +44,7 @@ fn build_rust_src() -> Job {
fn build_react_ssr_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-ssr".into(),
prog: DEV_SSR_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -77,7 +87,7 @@ pub async fn watch() -> Result<()> {
if should_reload_rust_server {
println!(" Reloading...");
run_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
run_server.start();
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.13.0"
version = "0.14.4"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
@@ -31,7 +31,7 @@ either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.13.0"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.14.4"}
# Match the same version used by axum
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
+1 -2
View File
@@ -15,9 +15,8 @@ pub use payload::Payload;
pub use request::Request;
pub use response::{Props, Response};
pub use server::Server;
pub use tuono_lib_macros::handler;
pub use tuono_lib_macros::{api, handler};
// Re-exports
pub use axum;
pub use reqwest;
pub use tokio;
+12 -3
View File
@@ -9,6 +9,16 @@ use std::path::PathBuf;
/// update the SSR result without reloading the whole server.
pub struct Js;
#[cfg(target_os = "windows")]
const PROD_BUNDLE_PATH: &str = ".\\out\\server\\prod-server.js";
#[cfg(target_os = "windows")]
const DEV_BUNDLE_PATH: &str = ".\\.tuono\\server\\dev-server.js";
#[cfg(not(target_os = "windows"))]
const PROD_BUNDLE_PATH: &str = "./out/server/prod-server.js";
#[cfg(not(target_os = "windows"))]
const DEV_BUNDLE_PATH: &str = "./.tuono/server/dev-server.js";
impl Js {
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
@@ -27,7 +37,7 @@ impl ProdJs {
thread_local! {
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
Ssr::from(
read_to_string(PathBuf::from("./out/server/prod-server.js")).expect("Server bundle not found"), ""
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
}
@@ -38,8 +48,7 @@ struct DevJs;
impl DevJs {
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
Ssr::from(
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
.expect("Server bundle not found"),
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
"",
)
.unwrap()
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.13.0"
version = "0.14.4"
edition = "2021"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
+69
View File
@@ -0,0 +1,69 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat};
pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let http_method = parse_macro_input!(attrs as Ident)
.to_string()
.to_lowercase();
let api_fn_name = Ident::new(
&format!("{}__tuono_internal_api", http_method),
Span::call_site().into(),
);
let fn_name = &item.sig.ident;
let return_type = &item.sig.output;
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
// Fn Arguments minus the first which always is the request
for (i, arg) in item.sig.inputs.iter().enumerate() {
if i == 0 {
axum_arguments.insert(i, params_argument());
continue;
}
if i == 1 {
axum_arguments.insert(1, create_struct_fn_arg())
}
if let FnArg::Typed(pat_type) = arg {
let index = i - 1;
let argument_name = *pat_type.pat.clone();
argument_names.insert(index, argument_name.clone());
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
let application_state_import = import_main_application_state(argument_names.clone());
quote! {
#application_state_import
#item
pub async fn #api_fn_name(#axum_arguments)#return_type {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), #argument_names).await
}
}
.into()
}
+6 -43
View File
@@ -1,51 +1,14 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_macro_input, parse_quote, FnArg, ItemFn, ItemUse, Pat, Stmt};
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
fn create_struct_fn_arg() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
})
.unwrap()
}
fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<ItemUse> {
if !argument_names.is_empty() {
let use_item: ItemUse = parse_quote!(let ApplicationState { #argument_names } = state;);
return Some(use_item);
}
None
}
fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let local: Stmt = parse_quote!(
use crate::tuono_main_state::ApplicationState;
);
return Some(local);
}
None
}
fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
+7
View File
@@ -1,9 +1,16 @@
extern crate proc_macro;
use proc_macro::TokenStream;
mod api;
mod handler;
mod utils;
#[proc_macro_attribute]
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
handler::handler_core(args, item)
}
#[proc_macro_attribute]
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
api::api_core(args, item)
}
+47
View File
@@ -0,0 +1,47 @@
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_quote, FnArg, Pat, Stmt};
pub fn create_struct_fn_arg() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
})
.unwrap()
}
pub fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let local: Stmt = parse_quote!(
use crate::tuono_main_state::ApplicationState;
);
return Some(local);
}
None
}
pub fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names, .. } = state;);
return Some(use_item);
}
None
}
pub fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
pub fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
-11
View File
@@ -15,14 +15,3 @@ $ tuono new [NAME] --template [TEMPLATE]
```
`[TEMPLATE]` is the folder name.
## Troubleshooting for contributors
There is a common error that happens during example development due to
pnpm workspace about the findings of different react versions.
To overcome this issue run within the single example folder:
```
pnpm install --ignore-workspace && pnpm upgrade --save
```
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "tuono-app"
version = "0.0.1"
edition = "2021"
@@ -1,6 +1,6 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"name": "tuono-app",
"description": "Basic tuono application",
"version": "0.0.1",
"dependencies": {
"react": "18.3.1",

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,7 @@
use tuono_lib::Request;
use tuono_lib::axum::http::StatusCode;
#[tuono_lib::api(GET)]
pub async fn my_get_request(_req: Request) -> StatusCode {
StatusCode::OK
}
@@ -23,7 +23,7 @@ body {
}
main {
width: 673px;
max-width: 673px;
margin: 20px auto;
}
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "tuono-tutorial"
version = "0.0.1"
edition = "2021"
+9
View File
@@ -0,0 +1,9 @@
# Tuono tutorial
This project is the outcome of the [tuono tutorial](https://tuono.dev/documentation/tutorial)
If you want to directly install it you can run:
```bash
$ tuono new my-project -t tuono-tutorial
```
@@ -1,6 +1,6 @@
{
"name": "tuono-tutorial",
"description": "The react/rust fullstack framework",
"description": "The basic tutorial for tuono applications",
"version": "0.0.1",
"dependencies": {
"react": "18.3.1",

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -1,6 +1,6 @@
// src/routes/index.rs
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
@@ -1,6 +1,6 @@
// src/routes/pokemons/[pokemon].rs
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
-9
View File
@@ -1,9 +0,0 @@
# Tuono tutorial
This project is the outcome of the [tuono tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
If you want to directly install it you can run:
```bash
$ tuono new my-project -t tutorial
```
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "with-mdx"
version = "0.0.1"
edition = "2021"
@@ -1,6 +1,6 @@
{
"name": "tuono",
"description": "The react/rust fullstack framework",
"name": "with-mdx",
"description": "Tuono example combined with mdx",
"version": "0.0.1",
"dependencies": {
"@mdx-js/react": "^3.0.1",

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "workspace",
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
"scripts": {
"dev": "turbo dev --filter=./packages/*",
"build": "turbo build --filter=./packages/*",
@@ -14,7 +14,9 @@
"docs:format": "turbo format --filter=documentation",
"docs:format:check": "turbo format:check --filter=documentation",
"docs:types": "turbo types --filter=documentation",
"check-all": "turbo build lint format:check types"
"repo:root:format:check": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --check",
"repo:root:format": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --write",
"check-all": "turbo build lint format:check types --filter=!./examples"
},
"author": "Valerio Ageno",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.13.0",
"version": "0.14.4",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -2,40 +2,28 @@ import fs from 'fs/promises'
import { describe, it, expect } from 'vitest'
import { routeGenerator } from '../src/generator'
function makeFolderDir(folder: string) {
return process.cwd() + `/tests/generator/${folder}`
}
async function getRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
return await fs.readFile(dir + '/routeTree.gen.ts', 'utf-8')
}
async function getExpectedRouteTreeFileText(folder: string) {
const dir = makeFolderDir(folder)
const location = dir + '/routeTree.expected.ts'
return await fs.readFile(location, 'utf-8')
}
describe('generator works', async () => {
const folderNames = await fs.readdir(process.cwd() + '/tests/generator')
it.each(folderNames.map((folder) => [folder]))(
it.each(folderNames)(
'should wire-up the routes for a "%s" tree',
async (folderName) => {
const currentFolder = `${process.cwd()}/tests/generator/${folderName}`
const testDirPath = `${process.cwd()}/tests/generator/${folderName}`
await routeGenerator({
folderName: `${currentFolder}/routes`,
generatedRouteTree: `${currentFolder}/routeTree.gen.ts`,
folderName: `${testDirPath}/routes`,
generatedRouteTree: `${testDirPath}/routeTree.gen.ts`,
})
const [expectedRouteTree, generatedRouteTree] = await Promise.all([
getExpectedRouteTreeFileText(folderName),
getRouteTreeFileText(folderName),
])
const generatedFilePath = `${testDirPath}/routeTree.gen.ts`
const expectedFilePath = `${testDirPath}/routeTree.expected.ts`
expect(generatedRouteTree).equal(expectedRouteTree)
const generatedFileContent = await fs.readFile(generatedFilePath, 'utf-8')
await expect(generatedFileContent).toMatchFileSnapshot(
expectedFilePath,
`${generatedFilePath} content should be equal to ${expectedFilePath}`,
)
},
)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.13.0",
"version": "0.14.4",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-router",
"version": "0.13.0",
"version": "0.14.4",
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
+2 -2
View File
@@ -1,4 +1,4 @@
import type { JSX } from 'react'
import type { ComponentType as ReactComponentType } from 'react'
import { trimPath, trimPathRight } from './utils'
import type { Route } from './route'
@@ -12,7 +12,7 @@ interface CreateRouter {
}
interface RouteOptions {
component?: () => JSX.Element
component?: ReactComponentType
hasHandler?: boolean
routeTree?: RouteTree
}
+3 -1
View File
@@ -1,3 +1,5 @@
import type { ComponentType as ReactComponentType } from 'react'
export interface Segment {
type: 'pathname' | 'param' | 'wildcard'
value: string
@@ -17,6 +19,6 @@ export interface RouteProps {
isLoading: boolean
}
export type RouteComponent = ((props: RouteProps) => JSX.Element) & {
export type RouteComponent = ReactComponentType<RouteProps> & {
preload: () => void
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono",
"version": "0.13.0",
"version": "0.14.4",
"description": "Superfast React fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
+509 -265
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
packages:
- 'packages/*'
- 'apps/*'
- '!**/examples/**'
- 'examples/*'

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