mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 13:52:46 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcb2c91823 | |||
| 9058f209dd | |||
| 1f172d583e | |||
| 439b4757df | |||
| 9f8809bd51 | |||
| 4e326650cc | |||
| e27be27692 | |||
| 88eb2a1897 | |||
| 8c84ef73c8 | |||
| 859e49f16c | |||
| 14ba8b0a23 | |||
| d7c70f75f3 | |||
| 2760e3ea08 | |||
| f2f041fe55 | |||
| 9ec9700ed4 | |||
| c6b047fc02 | |||
| 6727264e39 | |||
| d1a5fe234d | |||
| e84c7cd276 | |||
| 087813fab9 | |||
| 3a4e7398c6 | |||
| 1630fd7e71 | |||
| 4aa99beaff | |||
| eaa0ba28d8 | |||
| 3f3e1120b2 |
@@ -28,4 +28,4 @@ jobs:
|
|||||||
uses: ./.github/actions/install-node-dependencies
|
uses: ./.github/actions/install-node-dependencies
|
||||||
|
|
||||||
- name: Test project
|
- name: Test project
|
||||||
run: pnpm repo:root:format
|
run: pnpm repo:root:format:check
|
||||||
|
|||||||
+26
-18
@@ -14,6 +14,32 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
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: 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:
|
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
|
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
|
name: Check format and lint rust crates
|
||||||
@@ -25,21 +51,3 @@ jobs:
|
|||||||
|
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
- run: cargo clippy -- -D warnings
|
- 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
|
|
||||||
|
|||||||
@@ -1,23 +1,66 @@
|
|||||||
name: Spell Checker
|
name: Spell Checker
|
||||||
|
|
||||||
on: [pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
spell_checking:
|
spell_checking:
|
||||||
name: runner / LanguageTools
|
name: LanguageTool
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: reviewdog/action-languagetool@v1
|
|
||||||
with:
|
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].
|
# 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.
|
# Change reporter level if you need.
|
||||||
level: 'error'
|
level: 'warning'
|
||||||
patterns: '**/*.md **/*.txt **/*.mdx'
|
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_categories: ''
|
||||||
disabled_rules: ''
|
disabled_rules: ''
|
||||||
|
|||||||
@@ -11,11 +11,20 @@ on:
|
|||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
|
|
||||||
jobs:
|
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
|
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
|
name: Check build and test
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: true
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- 'ubuntu-latest'
|
||||||
|
- 'macos-latest'
|
||||||
|
- 'windows-latest'
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -30,7 +39,7 @@ jobs:
|
|||||||
- name: Test project
|
- name: Test project
|
||||||
run: pnpm test
|
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
|
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
|
name: Check format and lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
**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/).
|
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||||
Why Tuono? Just a badass name.
|
Why Tuono? Just a badass name.
|
||||||
@@ -51,9 +52,6 @@ Tuono runs the server without any intermediary runtime. This enables impressive
|
|||||||
|
|
||||||
## Getting started
|
## 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.
|
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/)
|
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
|
||||||
package manager.
|
package manager.
|
||||||
@@ -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
|
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
|
||||||
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
||||||
|
|
||||||
Then, to run the local development environment, run 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
|
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.
|
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 |
@@ -90,12 +90,19 @@ export default function Sidebar({ close }: SidebarProps): JSX.Element {
|
|||||||
onClick={close}
|
onClick={close}
|
||||||
/>
|
/>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
label="Contributing"
|
label="Contributing"
|
||||||
href="/documentation/contributing"
|
href="/documentation/contributing"
|
||||||
onClick={close}
|
|
||||||
leftSection="✨"
|
leftSection="✨"
|
||||||
/>
|
onClick={close}
|
||||||
|
>
|
||||||
|
<SidebarLink
|
||||||
|
href="/documentation/contributing/local-development"
|
||||||
|
label="Local development"
|
||||||
|
onClick={close}
|
||||||
|
/>
|
||||||
|
</SidebarLink>
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
|||||||
|
|
||||||
The main reason Tuono is fast is that 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 define them, you need to fill the `ApplicationState` struct in the `./src/main.rs` file, and
|
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.
|
they will be automatically available in all the handlers you will define across the application.
|
||||||
|
|
||||||
```rs
|
```rs
|
||||||
// src/main.rs
|
// src/app.rs
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ApplicationState {
|
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.
|
||||||
@@ -70,7 +70,7 @@ Let's fix these in the next section.
|
|||||||
|
|
||||||
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.
|
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ serde = { version = "1.0.202", features = ["derive"] }
|
|||||||
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
|
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
|
||||||
> (similarly as the package.json for Javascript).
|
> (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
|
```rs
|
||||||
// Import here the just added reqwest library
|
// Import here the just added reqwest library
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono"
|
name = "tuono"
|
||||||
version = "0.13.3"
|
version = "0.14.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||||
description = "Superfast React fullstack framework"
|
description = "Superfast React fullstack framework"
|
||||||
@@ -32,3 +32,4 @@ regex = "1.10.4"
|
|||||||
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
fs_extra = "1.3.0"
|
fs_extra = "1.3.0"
|
||||||
|
http = "1.1.0"
|
||||||
|
|||||||
+136
-26
@@ -1,5 +1,7 @@
|
|||||||
use glob::glob;
|
use glob::glob;
|
||||||
use glob::GlobError;
|
use glob::GlobError;
|
||||||
|
use http::Method;
|
||||||
|
use std::collections::hash_set::HashSet;
|
||||||
use std::collections::{hash_map::Entry, HashMap};
|
use std::collections::{hash_map::Entry, HashMap};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
@@ -14,15 +16,25 @@ use crate::route::Route;
|
|||||||
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
|
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
|
||||||
const IGNORE_FILES: [&str; 1] = ["__root"];
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub route_map: HashMap<String, Route>,
|
pub route_map: HashMap<String, Route>,
|
||||||
pub base_path: PathBuf,
|
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> {
|
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||||
let file = File::open(base_path.join("src/main.rs"))?;
|
let file = File::open(base_path.join("src/app.rs"))?;
|
||||||
let mut buf_reader = BufReader::new(file);
|
let mut buf_reader = BufReader::new(file);
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
buf_reader.read_to_string(&mut contents)?;
|
buf_reader.read_to_string(&mut contents)?;
|
||||||
@@ -31,12 +43,12 @@ fn has_main_file(base_path: PathBuf) -> std::io::Result<bool> {
|
|||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new() -> Self {
|
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 {
|
let mut app = App {
|
||||||
route_map: HashMap::new(),
|
route_map: HashMap::new(),
|
||||||
base_path: base_path.clone(),
|
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();
|
app.collect_routes();
|
||||||
@@ -45,18 +57,31 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_routes(&mut self) {
|
fn collect_routes(&mut self) {
|
||||||
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
|
glob(
|
||||||
.expect("Failed to read glob pattern")
|
self.base_path
|
||||||
.for_each(|entry| {
|
.join("src/routes/**/*.*")
|
||||||
if self.should_collect_route(&entry) {
|
.to_str()
|
||||||
self.collect_route(entry)
|
.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 {
|
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
|
||||||
let file_extension = entry.as_ref().unwrap().extension().unwrap();
|
let file_extension = entry
|
||||||
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
|
.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) {
|
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
|
||||||
return false;
|
return false;
|
||||||
@@ -69,17 +94,23 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
||||||
let entry = path_buf.unwrap();
|
let entry = path_buf.expect("Failed to read glob path");
|
||||||
let base_path_str = self.base_path.to_str().unwrap();
|
|
||||||
|
let base_path_str = self
|
||||||
|
.base_path
|
||||||
|
.to_str()
|
||||||
|
.expect("Failed to read as str base_path");
|
||||||
let path = entry
|
let path = entry
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.expect("Failed to read entry as str")
|
||||||
.replace(&format!("{base_path_str}/src/routes"), "")
|
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
|
||||||
|
// Cleanup windows paths
|
||||||
|
.replace("\\", "/")
|
||||||
.replace(".rs", "")
|
.replace(".rs", "")
|
||||||
.replace(".mdx", "")
|
.replace(".mdx", "")
|
||||||
.replace(".tsx", "");
|
.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()) {
|
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||||
let mut route = Route::new(path);
|
let mut route = Route::new(path);
|
||||||
route.update_axum_info();
|
route.update_axum_info();
|
||||||
@@ -101,7 +132,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_react_prod(&self) {
|
pub fn build_react_prod(&self) {
|
||||||
Command::new("./node_modules/.bin/tuono-build-prod")
|
Command::new(BUILD_JS_SCRIPT)
|
||||||
.output()
|
.output()
|
||||||
.expect("Failed to build the react source");
|
.expect("Failed to build the react source");
|
||||||
}
|
}
|
||||||
@@ -115,6 +146,24 @@ impl App {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to run the rust server")
|
.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)]
|
#[cfg(test)]
|
||||||
@@ -125,8 +174,27 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_collect_routes() {
|
fn should_collect_routes() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
@@ -173,8 +241,25 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_create_multi_level_axum_paths() {
|
fn should_create_multi_level_axum_paths() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
@@ -199,7 +284,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.route_map
|
app.route_map
|
||||||
.get(path)
|
.get(path)
|
||||||
.unwrap()
|
.expect("Failed to get route path")
|
||||||
.axum_info
|
.axum_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -251,8 +336,23 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_parse_routes_with_server_handler() {
|
fn should_correctly_parse_routes_with_server_handler() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/about.tsx",
|
"/home/user/Documents/tuono/src/routes/about.tsx",
|
||||||
@@ -269,9 +369,19 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|(path, expected_has_server_handler)| {
|
.for_each(|(path, expected_has_server_handler)| {
|
||||||
if 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 {
|
} 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())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ use crate::scaffold_project;
|
|||||||
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
||||||
use crate::watch;
|
use crate::watch;
|
||||||
|
|
||||||
|
const TUONO_PORT: u16 = 3000;
|
||||||
|
const VITE_PORT: u16 = 3001;
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum Actions {
|
enum Actions {
|
||||||
/// Start the development environment
|
/// Start the development environment
|
||||||
@@ -47,12 +50,41 @@ fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
|||||||
Ok(())
|
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<()> {
|
pub fn app() -> std::io::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
match args.action {
|
match args.action {
|
||||||
Actions::Dev => {
|
Actions::Dev => {
|
||||||
|
check_ports(Mode::Dev);
|
||||||
|
|
||||||
init_tuono_folder(Mode::Dev)?;
|
init_tuono_folder(Mode::Dev)?;
|
||||||
|
|
||||||
watch::watch().unwrap();
|
watch::watch().unwrap();
|
||||||
}
|
}
|
||||||
Actions::Build { ssg } => {
|
Actions::Build { ssg } => {
|
||||||
@@ -68,6 +100,8 @@ pub fn app() -> std::io::Result<()> {
|
|||||||
app.build_react_prod();
|
app.build_react_prod();
|
||||||
|
|
||||||
if ssg {
|
if ssg {
|
||||||
|
check_ports(Mode::Prod);
|
||||||
|
|
||||||
println!("SSG: generation started");
|
println!("SSG: generation started");
|
||||||
|
|
||||||
let static_dir = PathBuf::from("out/static");
|
let static_dir = PathBuf::from("out/static");
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
use fs_extra::dir::create_all;
|
use fs_extra::dir::create_all;
|
||||||
|
use http::Method;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
fn has_dynamic_path(route: &str) -> bool {
|
fn has_dynamic_path(route: &str) -> bool {
|
||||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||||
regex.is_match(route)
|
regex.is_match(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct AxumInfo {
|
pub struct AxumInfo {
|
||||||
// Path for importing the module
|
// Path for importing the module
|
||||||
pub module_import: String,
|
pub module_import: String,
|
||||||
@@ -65,11 +67,56 @@ impl AxumInfo {
|
|||||||
// TODO: to be extended with common scenarios
|
// TODO: to be extended with common scenarios
|
||||||
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
|
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 {
|
pub struct Route {
|
||||||
path: String,
|
path: String,
|
||||||
pub is_dynamic: bool,
|
pub is_dynamic: bool,
|
||||||
pub axum_info: Option<AxumInfo>,
|
pub axum_info: Option<AxumInfo>,
|
||||||
|
pub api_data: Option<ApiData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
@@ -78,9 +125,14 @@ impl Route {
|
|||||||
path: cleaned_path.clone(),
|
path: cleaned_path.clone(),
|
||||||
axum_info: None,
|
axum_info: None,
|
||||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
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) {
|
pub fn update_axum_info(&mut self) {
|
||||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,17 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
|
|||||||
.collect::<Vec<&GithubFile>>();
|
.collect::<Vec<&GithubFile>>();
|
||||||
|
|
||||||
if new_project_files.is_empty() {
|
if new_project_files.is_empty() {
|
||||||
println!("Template not found: {template}");
|
eprintln!("Error: Template '{template}' not found");
|
||||||
return;
|
println!("Hint: you can view the available templates at https://github.com/Valerioageno/tuono/tree/main/examples");
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if folder != "." {
|
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();
|
create_dir(&folder).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
|||||||
module_import,
|
module_import,
|
||||||
} = axum_info.as_ref().unwrap();
|
} = axum_info.as_ref().unwrap();
|
||||||
|
|
||||||
route_declarations.push_str(&format!(
|
if !route.is_api() {
|
||||||
r#".route("{axum_route}", get({module_import}::route))"#
|
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!(
|
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
|
||||||
));
|
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("/*MODE*/", mode.as_str())
|
||||||
.replace(
|
.replace(
|
||||||
"//MAIN_FILE_IMPORT//",
|
"//MAIN_FILE_IMPORT//",
|
||||||
if app.has_main_file {
|
if app.has_app_state {
|
||||||
r#"#[path="../src/main.rs"]
|
r#"#[path="../src/app.rs"]
|
||||||
mod tuono_main_state;
|
mod tuono_main_state;
|
||||||
"#
|
"#
|
||||||
} else {
|
} else {
|
||||||
@@ -149,7 +159,7 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"//MAIN_FILE_DEFINITION//",
|
"//MAIN_FILE_DEFINITION//",
|
||||||
if app.has_main_file {
|
if app.has_app_state {
|
||||||
"let user_custom_state = tuono_main_state::main();"
|
"let user_custom_state = tuono_main_state::main();"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
@@ -157,27 +167,23 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"//MAIN_FILE_USAGE//",
|
"//MAIN_FILE_USAGE//",
|
||||||
if app.has_main_file {
|
if app.has_app_state {
|
||||||
".with_state(user_custom_state)"
|
".with_state(user_custom_state)"
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let has_server_handlers = app
|
let mut import_http_handler = String::new();
|
||||||
.route_map
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, route)| route.axum_info.is_some())
|
|
||||||
.collect::<Vec<(&String, &Route)>>()
|
|
||||||
.is_empty();
|
|
||||||
|
|
||||||
if !has_server_handlers {
|
let used_http_methods = app.get_used_http_methods();
|
||||||
return src.replace(
|
|
||||||
"// AXUM_GET_ROUTE_HANDLER",
|
for method in used_http_methods.into_iter() {
|
||||||
"use tuono_lib::axum::routing::get;",
|
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<()> {
|
pub fn check_tuono_folder() -> io::Result<()> {
|
||||||
|
|||||||
@@ -9,10 +9,20 @@ use watchexec_supervisor::job::{start_job, Job};
|
|||||||
use crate::mode::Mode;
|
use crate::mode::Mode;
|
||||||
use crate::source_builder::bundle_axum_source;
|
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 {
|
fn watch_react_src() -> Job {
|
||||||
start_job(Arc::new(Command {
|
start_job(Arc::new(Command {
|
||||||
program: Program::Exec {
|
program: Program::Exec {
|
||||||
prog: "node_modules/.bin/tuono-dev-watch".into(),
|
prog: DEV_WATCH_BIN_SRC.into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
},
|
},
|
||||||
options: Default::default(),
|
options: Default::default(),
|
||||||
@@ -34,7 +44,7 @@ fn build_rust_src() -> Job {
|
|||||||
fn build_react_ssr_src() -> Job {
|
fn build_react_ssr_src() -> Job {
|
||||||
start_job(Arc::new(Command {
|
start_job(Arc::new(Command {
|
||||||
program: Program::Exec {
|
program: Program::Exec {
|
||||||
prog: "node_modules/.bin/tuono-dev-ssr".into(),
|
prog: DEV_SSR_BIN_SRC.into(),
|
||||||
args: vec![],
|
args: vec![],
|
||||||
},
|
},
|
||||||
options: Default::default(),
|
options: Default::default(),
|
||||||
@@ -77,7 +87,7 @@ pub async fn watch() -> Result<()> {
|
|||||||
if should_reload_rust_server {
|
if should_reload_rust_server {
|
||||||
println!(" Reloading...");
|
println!(" Reloading...");
|
||||||
run_server.stop();
|
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();
|
run_server.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono_lib"
|
name = "tuono_lib"
|
||||||
version = "0.13.3"
|
version = "0.14.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||||
description = "Superfast React fullstack framework"
|
description = "Superfast React fullstack framework"
|
||||||
@@ -31,7 +31,7 @@ either = "1.13.0"
|
|||||||
tower-http = {version = "0.6.0", features = ["fs"]}
|
tower-http = {version = "0.6.0", features = ["fs"]}
|
||||||
colored = "2.1.0"
|
colored = "2.1.0"
|
||||||
|
|
||||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.13.3"}
|
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.14.3"}
|
||||||
# Match the same version used by axum
|
# Match the same version used by axum
|
||||||
tokio-tungstenite = "0.24.0"
|
tokio-tungstenite = "0.24.0"
|
||||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub use payload::Payload;
|
|||||||
pub use request::Request;
|
pub use request::Request;
|
||||||
pub use response::{Props, Response};
|
pub use response::{Props, Response};
|
||||||
pub use server::Server;
|
pub use server::Server;
|
||||||
pub use tuono_lib_macros::handler;
|
pub use tuono_lib_macros::{api, handler};
|
||||||
|
|
||||||
// Re-exports
|
// Re-exports
|
||||||
pub use axum;
|
pub use axum;
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ use std::path::PathBuf;
|
|||||||
/// update the SSR result without reloading the whole server.
|
/// update the SSR result without reloading the whole server.
|
||||||
pub struct Js;
|
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 {
|
impl Js {
|
||||||
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
|
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
|
||||||
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
|
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
|
||||||
@@ -27,7 +37,7 @@ impl ProdJs {
|
|||||||
thread_local! {
|
thread_local! {
|
||||||
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
||||||
Ssr::from(
|
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()
|
).unwrap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -38,8 +48,7 @@ struct DevJs;
|
|||||||
impl DevJs {
|
impl DevJs {
|
||||||
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
|
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
|
||||||
Ssr::from(
|
Ssr::from(
|
||||||
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
|
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
|
||||||
.expect("Server bundle not found"),
|
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono_lib_macros"
|
name = "tuono_lib_macros"
|
||||||
version = "0.13.3"
|
version = "0.14.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Superfast React fullstack framework"
|
description = "Superfast React fullstack framework"
|
||||||
homepage = "https://tuono.dev"
|
homepage = "https://tuono.dev"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::punctuated::Punctuated;
|
use syn::punctuated::Punctuated;
|
||||||
use syn::token::Comma;
|
use syn::token::Comma;
|
||||||
use syn::{parse2, parse_macro_input, parse_quote, FnArg, ItemFn, 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<Stmt> {
|
|
||||||
if !argument_names.is_empty() {
|
|
||||||
let local: Stmt = parse_quote!(
|
|
||||||
use crate::tuono_main_state::ApplicationState;
|
|
||||||
);
|
|
||||||
return Some(local);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
let item = parse_macro_input!(item as ItemFn);
|
let item = parse_macro_input!(item as ItemFn);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
mod api;
|
||||||
mod handler;
|
mod handler;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
handler::handler_core(args, item)
|
handler::handler_core(args, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
api::api_core(args, item)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
"packageManager": "pnpm@9.14.1+sha512.7f1de9cffea40ff4594c48a94776112a0db325e81fb18a9400362ff7b7247f4fbd76c3011611c9f8ac58743c3dc526017894e07948de9b72052f874ee2edfdcd",
|
"packageManager": "pnpm@9.14.2+sha512.6e2baf77d06b9362294152c851c4f278ede37ab1eba3a55fda317a4a17b209f4dbb973fb250a77abc463a341fcb1f17f17cfa24091c4eb319cda0d9b84278387",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "turbo dev --filter=./packages/*",
|
"dev": "turbo dev --filter=./packages/*",
|
||||||
"build": "turbo build --filter=./packages/*",
|
"build": "turbo build --filter=./packages/*",
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"docs:format": "turbo format --filter=documentation",
|
"docs:format": "turbo format --filter=documentation",
|
||||||
"docs:format:check": "turbo format:check --filter=documentation",
|
"docs:format:check": "turbo format:check --filter=documentation",
|
||||||
"docs:types": "turbo types --filter=documentation",
|
"docs:types": "turbo types --filter=documentation",
|
||||||
|
"repo:root:format:check": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --check",
|
||||||
"repo:root:format": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --write",
|
"repo:root:format": "prettier . !./apps/** !./assets/** !./benches/** !./crates !./examples !./packages/** --write",
|
||||||
"check-all": "turbo build lint format:check types --filter=!./examples"
|
"check-all": "turbo build lint format:check types --filter=!./examples"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tuono-fs-router-vite-plugin",
|
"name": "tuono-fs-router-vite-plugin",
|
||||||
"version": "0.13.3",
|
"version": "0.14.3",
|
||||||
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
||||||
"homepage": "https://tuono.dev",
|
"homepage": "https://tuono.dev",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,40 +2,28 @@ import fs from 'fs/promises'
|
|||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { routeGenerator } from '../src/generator'
|
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 () => {
|
describe('generator works', async () => {
|
||||||
const folderNames = await fs.readdir(process.cwd() + '/tests/generator')
|
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',
|
'should wire-up the routes for a "%s" tree',
|
||||||
async (folderName) => {
|
async (folderName) => {
|
||||||
const currentFolder = `${process.cwd()}/tests/generator/${folderName}`
|
const testDirPath = `${process.cwd()}/tests/generator/${folderName}`
|
||||||
|
|
||||||
await routeGenerator({
|
await routeGenerator({
|
||||||
folderName: `${currentFolder}/routes`,
|
folderName: `${testDirPath}/routes`,
|
||||||
generatedRouteTree: `${currentFolder}/routeTree.gen.ts`,
|
generatedRouteTree: `${testDirPath}/routeTree.gen.ts`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [expectedRouteTree, generatedRouteTree] = await Promise.all([
|
const generatedFilePath = `${testDirPath}/routeTree.gen.ts`
|
||||||
getExpectedRouteTreeFileText(folderName),
|
const expectedFilePath = `${testDirPath}/routeTree.expected.ts`
|
||||||
getRouteTreeFileText(folderName),
|
|
||||||
])
|
|
||||||
|
|
||||||
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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tuono-lazy-fn-vite-plugin",
|
"name": "tuono-lazy-fn-vite-plugin",
|
||||||
"version": "0.13.3",
|
"version": "0.14.3",
|
||||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
||||||
"homepage": "https://tuono.dev",
|
"homepage": "https://tuono.dev",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tuono-router",
|
"name": "tuono-router",
|
||||||
"version": "0.13.3",
|
"version": "0.14.3",
|
||||||
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
||||||
"homepage": "https://tuono.dev",
|
"homepage": "https://tuono.dev",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tuono",
|
"name": "tuono",
|
||||||
"version": "0.13.3",
|
"version": "0.14.3",
|
||||||
"description": "Superfast React fullstack framework",
|
"description": "Superfast React fullstack framework",
|
||||||
"homepage": "https://tuono.dev",
|
"homepage": "https://tuono.dev",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Generated
+322
-190
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user