Compare commits

..

52 Commits

Author SHA1 Message Date
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
Valerio Ageno df92ef5296 Add support for custom global state (#111)
* feat: add 'has_main_file' to App struct

* feat: handle custom state from the main.rs file

* feat: update tutorial template with new setup

* fix: apply clippy hints

* feat: add ApplicationState mention to documentation website

* feat: update version to v0.13.0
2024-11-17 16:28:56 +01:00
Valerio Ageno 253e0a8378 feat(doc): keep social buttons on mobile website 2024-11-17 12:26:58 +01:00
Valerio Ageno a9fbe4eddd feat: add discord channel invite to documentation website 2024-11-17 12:19:25 +01:00
Valerio Ageno cfccc638c6 feat: update version to v0.12.4 2024-11-16 19:23:21 +01:00
renovate[bot] 9f1460cc26 fix(deps): update mantine monorepo to v7.14.1 (#110) 2024-11-16 17:11:02 +01:00
Henry Chen f0a4541420 fix: handle hyphen in route (#109) 2024-11-16 17:10:37 +01:00
renovate[bot] d0ce482680 chore(deps): update dependency vitest to v2.1.5 (#105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:29:50 +01:00
renovate[bot] 40e2734a4e fix(deps): update dependency turbo to v2.3.0 (#106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:28:58 +01:00
Marco Pasqualetti 8552796343 chore: fix typo and improve name on CI (#107) 2024-11-16 11:47:53 +01:00
Marco Pasqualetti 6d72ffb876 ci: create install-node-dependencies action (#100) 2024-11-16 11:27:10 +01:00
Valerio Ageno 65faaf0183 feat: added website to crates/package manifest 2024-11-16 11:08:50 +01:00
Valerio Ageno 7f7c2e8d6f feat: update documentation tuono to v0.12.3 (#104) 2024-11-16 11:02:47 +01:00
109 changed files with 8630 additions and 365 deletions
+7 -7
View File
@@ -36,49 +36,49 @@ body:
id: tuono-version
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:
label: OS
placeholder: "[e.g. MacOS, Windows]"
placeholder: '[e.g. MacOS, Windows]'
validations:
required: false
- type: input
id: browser-version
attributes:
label: Browser
placeholder: "[e.g. chrome, safari]"
placeholder: '[e.g. chrome, safari]'
validations:
required: false
- type: input
id: node-version
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:
label: Rust version
placeholder: "[e.g. 1.79.0]"
placeholder: '[e.g. 1.79.0]'
validations:
required: false
- type: input
id: create-version
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:
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
@@ -0,0 +1,37 @@
name: Install NodeJS Dependencies
description: This is a composite GitHub Action that sets up pnpm, node and installs the project's dependencies.
inputs:
node-version:
description: 'Explicit node version. Otherwise fallback reading `.nvmrc`. Use in conjunction with matrix'
required: false
registry-url:
description: 'https://github.com/actions/setup-node?tab=readme-ov-file#usage'
required: false
default: ''
runs:
using: composite
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js (via input)
if: ${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
cache: 'pnpm'
- name: Setup Node.js (via .nvmrc)
if: ${{ !inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: ${{ inputs.registry-url }}
cache: 'pnpm'
- name: Install Dependencies
shell: bash
run: pnpm install --frozen-lockfile
+9 -22
View File
@@ -13,31 +13,22 @@ 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
steps:
- name: Check out code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.4
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
run: cargo install tuono@0.12.3
- name: Build project
working-directory: ./apps/documentation
@@ -45,20 +36,16 @@ jobs:
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
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Check formatting
run: pnpm docs:format:check
+4 -14
View File
@@ -15,27 +15,17 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Check out code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Setup rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.10.4
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
run: cargo install tuono@0.12.3
- name: Build project
working-directory: ./apps/documentation
+14 -28
View File
@@ -7,24 +7,14 @@ jobs:
publish-on-npm-dry-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
@@ -36,11 +26,14 @@ jobs:
publish-on-crates-dry-run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
@@ -52,27 +45,17 @@ jobs:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Clone root README.md on tuono package
run: cp README.md packages/tuono/README.md
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
@@ -85,11 +68,14 @@ jobs:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
+31
View File
@@ -0,0 +1,31 @@
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:
paths:
- '.github/**'
- './*'
pull_request:
paths:
- '.github/**'
- './*'
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
name: Check format
timeout-minutes: 15
runs-on: ubuntu-latest
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
+24 -11
View File
@@ -14,27 +14,40 @@ env:
CARGO_TERM_COLOR: always
jobs:
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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- 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:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
toolchain:
- stable
- beta
- nightly
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- 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
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: cargo fmt --all -- --check
- run: cargo clippy -- -D warnings
+53 -8
View File
@@ -1,21 +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:
- uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
- name: Checkout code
uses: actions/checkout@v4
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: ''
+20 -24
View File
@@ -11,27 +11,27 @@ on:
- 'packages/**'
jobs:
build-and-test:
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: 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: Check out code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Build project
run: pnpm build
@@ -39,22 +39,18 @@ jobs:
- name: Test project
run: pnpm test
fmt-lint-and-types:
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
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Check formatting
run: pnpm format:check
+3 -3
View File
@@ -1,12 +1,12 @@
## Node related ignores
node_modules/
dist/
.turbo/
*/**/routeTree.gen.ts
pnpm-lock.yaml
examples/playground/
examples/*/pnpm-lock.yaml
## Rust related ignores
+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"
+10 -10
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
+3
View File
@@ -5,6 +5,7 @@
# How to Contribute to Tuono
## Contributions
Any feature contribution or suggestion is strongly appreciated.
Since the current project size, there isn't yet a defined way to start a discussion. Consider [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.
+1 -1
View File
@@ -8,7 +8,7 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.12.0"
tuono_lib = "0.12.3"
glob = "0.3.1"
time = { version = "0.3", features = ["macros"] }
serde = { version = "1.0.202", features = ["derive"] }
+1 -1
View File
@@ -18,7 +18,7 @@
"clsx": "^2.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "npm:tuono@0.12.0"
"tuono": "npm:tuono@0.12.3"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 42 KiB

@@ -1,6 +1,10 @@
import type { JSX } from 'react'
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
import { IconBrandGithub, IconBook, IconBrandX } from '@tabler/icons-react'
import {
IconBrandGithub,
IconBook,
IconBrandDiscord,
} from '@tabler/icons-react'
import { Link } from 'tuono'
import ThemeBtn from '../theme-btn'
@@ -8,10 +12,11 @@ import ThemeBtn from '../theme-btn'
export default function Actions(): JSX.Element {
return (
<Flex gap={8}>
<Group gap={8} visibleFrom="sm">
<Group gap={8}>
<Button
href="/documentation"
component={Link}
href="/documentation"
visibleFrom="sm"
size="compact-lg"
rightSection={<IconBook />}
autoContrast
@@ -31,12 +36,12 @@ export default function Actions(): JSX.Element {
<ActionIcon
variant="default"
size="lg"
aria-label="We are also on X!"
href="https://x.com/tuono_dev"
aria-label="Join the Tuono's community on Discord!"
href="https://discord.com/invite/khQzPa654B"
target="_blank"
component="a"
>
<IconBrandX />
<IconBrandDiscord />
</ActionIcon>
</Group>
<ThemeBtn />
@@ -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
@@ -69,6 +73,12 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
/>
</SidebarLink>
<SidebarLink href="/documentation/cli" label="CLI" onClick={close} />
<SidebarLink
label="Application state"
href="/documentation/application-state"
onClick={close}
/>
<SidebarLink
label="Routing"
href="/documentation/routing"
@@ -80,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>
)
}
@@ -0,0 +1,55 @@
import MetaTags from '../../components/meta-tags'
<MetaTags
title="Tuono - Application state"
canonical="https://tuono.dev/documentation/application-state"
description="Learn how to add features to your Tuono application"
/>
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
# Application state
The main reason Tuono is fast is that it loads just the features that are needed for the project.
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/app.rs
#[derive(Clone)]
pub struct ApplicationState {
pub website_name: String,
pub base_path: String
}
pub fn main() -> ApplicationState {
let website_name = "tuono".to_string();
let base_path = "http://localhost:3000".to_string();
ApplicationState {
website_name,
base_path
}
}
```
> For the sake of simplicity here we just enable a `String` but you can add Database connections or
> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client).
Now the `ApplicationState` is available on all the handlers as following:
```rs
// src/routes/index.rs
#[tuono_lib::handler]
// The first argument always is the Request
// The ApplicationState arguments are optional. You can use just the ones you need
// to use in the handler (with no specific order).
async fn my_handler(req: Request, website_name: String) -> Response {
...
}
```
@@ -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.
@@ -28,7 +28,7 @@ Clear the `index.rs` file and paste:
```rust
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use reqwest::Client;
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
@@ -56,9 +56,74 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
}
```
> The first argument is always the request `req: Request` which contains the request data like the query parameters and the headers.
> The rest of the arguments are optional and they don't need to be specified
> if they are not used. Enabled out of the box a [Reqwest](https://docs.rs/reqwest/latest/reqwest/) HTTP client.
> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers.
> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional.
The terminal will complain now for two reasons:
1. We don't have imported any `reqwest` crate
2. The second argument `fetch: Client` has not been defined yet as global state.
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.
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.
To install it just run in your terminal:
```bash
$ cargo add reqwest
```
A new entry has just been added to your `Cargo.toml` file.
```diff
[package]
name = "tuono"
version = "0.0.1"
edition = "2021"
[[bin]]
name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.14.0" # the version might be different
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 you handle Rust's dependencies
> (similarly as the package.json for Javascript).
Now let's define the `ApplicationState` in the `./src/app.rs` file.
```rs
// Import here the just added reqwest library
use reqwest::Client;
#[derive(Clone)]
// Extend this struct with the feature you will need for your application
pub struct ApplicationState {
// This will be available to all your route handlers
pub fetch: Client,
}
pub fn main() -> ApplicationState {
let fetch = Client::new();
return ApplicationState { fetch };
}
```
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
Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste:
@@ -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"
/>
+6 -4
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono"
version = "0.12.3"
version = "0.14.3"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
@@ -20,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"
@@ -31,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"
+147 -17
View File
@@ -1,6 +1,11 @@
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::*;
use std::io::BufReader;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
@@ -11,19 +16,39 @@ 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_app_state: bool,
}
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)?;
Ok(contents.contains("pub fn main"))
}
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: base_path.clone(),
has_app_state: has_app_state(base_path).unwrap_or(false),
};
app.collect_routes();
@@ -32,7 +57,12 @@ impl App {
}
fn collect_routes(&mut self) {
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
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) {
@@ -42,8 +72,16 @@ impl App {
}
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;
@@ -56,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();
@@ -88,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");
}
@@ -102,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)]
@@ -112,13 +174,34 @@ 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",
"/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",
];
@@ -132,6 +215,11 @@ mod tests {
("/about", "about"),
("/posts/index", "posts_index"),
("/posts/[post]", "posts_dyn_post"),
("/posts/handle-this", "posts_handle_hyphen_this"),
(
"/posts/handle-this/[post]",
"posts_handle_hyphen_this_dyn_post",
),
("/posts/UPPERCASE", "posts_uppercase"),
("/sitemap.xml", "sitemap_dot_xml"),
];
@@ -153,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",
@@ -179,7 +284,7 @@ mod tests {
assert_eq!(
app.route_map
.get(path)
.unwrap()
.expect("Failed to get route path")
.axum_info
.as_ref()
.unwrap()
@@ -231,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",
@@ -249,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");
+56 -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,
@@ -32,6 +34,7 @@ impl AxumInfo {
.to_string()
.replace('/', "_")
.replace('.', "_dot_")
.replace('-', "_hyphen_")
.to_lowercase();
if axum_route.is_empty() {
@@ -47,6 +50,7 @@ impl AxumInfo {
.as_str()
.to_string()
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace(']', ""),
axum_route: axum_route.replace('[', ":").replace(']', ""),
@@ -63,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 {
@@ -76,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();
}
+52 -15
View File
@@ -42,12 +42,17 @@ const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
//MAIN_FILE_IMPORT//
#[tokio::main]
async fn main() {
println!("\n ⚡ Tuono v/*VERSION*/");
//MAIN_FILE_DEFINITION//
let router = Router::new()
// ROUTE_BUILDER
;
//MAIN_FILE_USAGE//;
Server::init(router, MODE).start().await
}
@@ -77,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
module_import,
} = axum_info.as_ref().unwrap();
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))"#
));
}
}
}
}
@@ -131,22 +146,44 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
&create_modules_declaration(&app.route_map),
)
.replace("/*VERSION*/", crate_version!())
.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;",
.replace("/*MODE*/", mode.as_str())
.replace(
"//MAIN_FILE_IMPORT//",
if app.has_app_state {
r#"#[path="../src/app.rs"]
mod tuono_main_state;
"#
} else {
""
},
)
.replace(
"//MAIN_FILE_DEFINITION//",
if app.has_app_state {
"let user_custom_state = tuono_main_state::main();"
} else {
""
},
)
.replace(
"//MAIN_FILE_USAGE//",
if app.has_app_state {
".with_state(user_custom_state)"
} else {
""
},
);
let mut import_http_handler = String::new();
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();
}
+4 -3
View File
@@ -1,9 +1,10 @@
[package]
name = "tuono_lib"
version = "0.12.3"
version = "0.14.3"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
@@ -30,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.12.3"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.14.3"}
# 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;
+4 -8
View File
@@ -14,12 +14,12 @@ const DEV_PUBLIC_DIR: &str = "public";
const PROD_PUBLIC_DIR: &str = "out/client";
pub struct Server {
router: Router<reqwest::Client>,
router: Router,
mode: Mode,
}
impl Server {
pub fn init(router: Router<reqwest::Client>, mode: Mode) -> Server {
pub fn init(router: Router, mode: Mode) -> Server {
Ssr::create_platform();
GLOBAL_MODE.set(mode).unwrap();
@@ -34,8 +34,6 @@ impl Server {
pub async fn start(&self) {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let fetch = reqwest::Client::new();
if self.mode == Mode::Dev {
println!(" Ready at: {}\n", "http://localhost:3000".blue().bold());
let router = self
@@ -47,8 +45,7 @@ impl Server {
.fallback_service(
ServeDir::new(DEV_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
)
.with_state(fetch);
);
axum::serve(listener, router)
.await
@@ -65,8 +62,7 @@ impl Server {
.fallback_service(
ServeDir::new(PROD_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
)
.with_state(fetch);
);
axum::serve(listener, router)
.await
+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()
+4 -5
View File
@@ -1,5 +1,5 @@
use axum::body::Body;
use axum::extract::{Path, State};
use axum::extract::Path;
use axum::http::{HeaderName, HeaderValue};
use axum::response::{IntoResponse, Response};
@@ -7,10 +7,9 @@ use reqwest::Client;
const VITE_URL: &str = "http://localhost:3001/vite-server";
pub async fn vite_reverse_proxy(
State(client): State<Client>,
Path(path): Path<String>,
) -> impl IntoResponse {
pub async fn vite_reverse_proxy(Path(path): Path<String>) -> impl IntoResponse {
let client = Client::new();
match client.get(format!("{VITE_URL}/{path}")).send().await {
Ok(res) => {
let mut response_builder = Response::builder().status(res.status().as_u16());
+3 -2
View File
@@ -1,8 +1,9 @@
[package]
name = "tuono_lib_macros"
version = "0.12.3"
version = "0.14.3"
edition = "2021"
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
readme = "../../README.md"
+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()
}
+21 -25
View File
@@ -1,31 +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, FnArg, ItemFn, Pat, Type};
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
fn create_struct_fn_arg(arg_name: Pat, arg_type: Type) -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(#arg_name): tuono_lib::axum::extract::State<#arg_type>
})
.unwrap()
}
fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
@@ -41,23 +24,33 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
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();
let argument_type = *pat_type.ty.clone();
argument_names.insert(index, argument_name.clone());
axum_arguments.insert(index, create_struct_fn_arg(argument_name, argument_type))
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
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 route(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
@@ -69,6 +62,9 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
pub async fn api(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
+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"
@@ -10,4 +10,5 @@ path = ".tuono/main.rs"
[dependencies]
tuono_lib = { path = "../../crates/tuono_lib/"}
serde = { version = "1.0.202", features = ["derive"] }
reqwest = "0.12.9"
+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

+11
View File
@@ -0,0 +1,11 @@
use reqwest::Client;
#[derive(Clone)]
pub struct ApplicationState {
pub fetch: Client,
}
pub fn main() -> ApplicationState {
let fetch = Client::new();
return ApplicationState { fetch };
}
@@ -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",
+2 -1
View File
@@ -1,7 +1,8 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.12.3",
"version": "0.14.3",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
@@ -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}`,
)
},
)
})
+2 -1
View File
@@ -1,7 +1,8 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.12.3",
"version": "0.14.3",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
+2 -1
View File
@@ -1,7 +1,8 @@
{
"name": "tuono-router",
"version": "0.12.3",
"version": "0.14.3",
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",

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