Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a2144a0d | |||
| adf7ba5391 | |||
| edb21ab969 | |||
| 4abc163a59 | |||
| 5bb9cf1e7c | |||
| a0cdf408a1 | |||
| 8467506915 | |||
| 704255774d | |||
| 10bb7b1cca | |||
| 7f82af3d3b | |||
| d7c729c035 | |||
| 51fa904f9b | |||
| 6191e55745 | |||
| c6e93c266a | |||
| bcf7e5b5ab | |||
| 466f122cc4 | |||
| 539484ea25 | |||
| db1cee25bf | |||
| 96502b18d0 | |||
| c491ece8c5 | |||
| c76464bf8a | |||
| 08b75b1a79 | |||
| 90da869277 | |||
| 80c4affaa8 | |||
| aa087193fe | |||
| af7e51f69b | |||
| 1b62d8ddc2 | |||
| 0839579733 | |||
| 7f9bf08f76 | |||
| 79ec1c6922 | |||
| c68246df83 | |||
| abc0ad7120 | |||
| 4356ca1cff | |||
| d6215d009c | |||
| 0b501e95a5 | |||
| f157ff37e0 | |||
| 11d425faa9 | |||
| 5fc68a1926 | |||
| ec4577c187 | |||
| 8052de8ad5 | |||
| 2fda959cd6 | |||
| 05fa8a5ae2 | |||
| a341b41e9a | |||
| a2e0a4d78a | |||
| 4fb1d6077c | |||
| 53ab927544 | |||
| 897cc2daf2 | |||
| 73ff31daf5 | |||
| c387a9e17e | |||
| 1f92931076 | |||
| d3105a1532 | |||
| 7f50f31837 | |||
| 4c3e90ea4b | |||
| 8d80fc4aa9 | |||
| 78a04ffcf1 |
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"reportUnusedDisableDirectives": true,
|
||||
"ignorePatterns": ["**/build", "**/dist"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"tsconfigRootDir": ".",
|
||||
"project": true,
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020,
|
||||
},
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
},
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ "prefer": "type-imports" },
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
"@typescript-eslint/method-signature-style": ["error", "property"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "typeParameter",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^(T|T[A-Z][A-Za-z]+)$",
|
||||
"match": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{ "ignoreParameters": true },
|
||||
],
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-cycle": "error",
|
||||
"import/no-duplicates": "off",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-unused-modules": "error",
|
||||
"import/order": [
|
||||
"off",
|
||||
{
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-case-declarations": "error",
|
||||
"no-empty": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
@@ -2,20 +2,29 @@ name: Documentation Website CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
# ⬇ Keep paths in sync with `typescript.yml`
|
||||
- '.github/**'
|
||||
- '*'
|
||||
- '!Cargo.toml'
|
||||
# ⬆ Keep paths in sync with `typescript.yml`
|
||||
- 'apps/documentation/**'
|
||||
pull_request:
|
||||
paths:
|
||||
# ⬇ Keep paths in sync with `typescript.yml`
|
||||
- '.github/**'
|
||||
- '*'
|
||||
- '!Cargo.toml'
|
||||
# ⬆ Keep paths in sync with `typescript.yml`
|
||||
- 'apps/documentation/**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check build
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -28,15 +37,14 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.12.3
|
||||
run: cargo install tuono@0.15.0
|
||||
|
||||
- name: Build project
|
||||
working-directory: ./apps/documentation
|
||||
run: tuono build --static
|
||||
|
||||
fmt-lint-and-types:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check format and lint
|
||||
code_check:
|
||||
name: Check format, lint and types
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
name: Deploy documentation website on AWS S3
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'main'
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
|
||||
jobs:
|
||||
deploy-documentation:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
@@ -25,7 +27,7 @@ jobs:
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.12.3
|
||||
run: cargo install tuono@0.16.0
|
||||
|
||||
- name: Build project
|
||||
working-directory: ./apps/documentation
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
name: Publish on npm and crates
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-on-npm-dry-run:
|
||||
publish_npm_dry_run:
|
||||
name: NPM publish (dry run)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -23,16 +26,18 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates-dry-run:
|
||||
publish_crates_dry_run:
|
||||
name: Crates publish (dry run)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
@@ -41,9 +46,11 @@ jobs:
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
publish-on-npm:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
publish_npm:
|
||||
name: NPM publish
|
||||
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -64,17 +71,19 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
publish_crates:
|
||||
name: Crates publish
|
||||
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
name: Repo root CI
|
||||
|
||||
# All workflows are triggered based on specific paths,
|
||||
# the root repository is something that is not applicable to rust nor typescript
|
||||
# so it has been moved in a standalone workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- '.github/**'
|
||||
- './*'
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- './*'
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
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
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -2,12 +2,16 @@ name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'Cargo.toml'
|
||||
- 'crates/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'Cargo.toml'
|
||||
- 'crates/**'
|
||||
|
||||
env:
|
||||
@@ -15,8 +19,7 @@ env:
|
||||
|
||||
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
|
||||
name: Build and test crates on ${{ matrix.os }} with rust ${{ matrix.toolchain }}
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -26,9 +29,15 @@ jobs:
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
toolchain:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- 'stable'
|
||||
|
||||
# Test `beta` and `nightly` toolchains only on `ubuntu`
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations
|
||||
include:
|
||||
- toolchain: 'beta'
|
||||
os: 'ubuntu-latest'
|
||||
- toolchain: 'nightly'
|
||||
os: 'ubuntu-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -36,12 +45,15 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ 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
|
||||
|
||||
@@ -49,5 +61,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Spell Checker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'main'
|
||||
|
||||
pull_request:
|
||||
|
||||
|
||||
@@ -2,17 +2,26 @@ name: Typescript CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
# ⬇ Keep paths in sync with `ci-documentation.yml`
|
||||
- '.github/**'
|
||||
- '*'
|
||||
- '!Cargo.toml'
|
||||
# ⬆ Keep paths in sync with `ci-documentation.yml`
|
||||
- 'packages/**'
|
||||
pull_request:
|
||||
paths:
|
||||
# ⬇ Keep paths in sync with `ci-documentation.yml`
|
||||
- '.github/**'
|
||||
- '*'
|
||||
- '!Cargo.toml'
|
||||
# ⬆ Keep paths in sync with `ci-documentation.yml`
|
||||
- 'packages/**'
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Check build and test
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -40,7 +49,6 @@ jobs:
|
||||
run: pnpm test
|
||||
|
||||
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
|
||||
|
||||
@@ -24,3 +24,6 @@ examples/*/pnpm-lock.yaml
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Files, generated by the code editors
|
||||
.vscode/
|
||||
@@ -1,5 +1,5 @@
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
# How to Contribute to Tuono
|
||||
@@ -7,15 +7,19 @@
|
||||
## 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.
|
||||
|
||||
We are currently managing the tasks' prioritization in the [Road to V1](https://github.com/orgs/tuono-labs/projects/2) project. You can pick any task that you see in the `ready` column that is not already assigned.
|
||||
|
||||
If you want to contribute on something that is in the `backlog` column, or it hasn't been scoped yet, please drop a message in the [#general](https://discord.com/invite/khQzPa654B) discord channel so we can all have visibility of it.
|
||||
|
||||
Consider [opening a new issue](https://github.com/tuono-labs/tuono/issues/new/choose) if you find a bug.
|
||||
|
||||
## 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.
|
||||
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/tuono-labs/tuono/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/tuono-labs/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?**
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ members = [
|
||||
]
|
||||
exclude = [
|
||||
"apps/documentation",
|
||||
"examples/wit-mdx",
|
||||
"examples/with-mdx",
|
||||
"examples/tuono-app",
|
||||
"examples/tuono-api",
|
||||
"examples/tuono-tutorial",
|
||||
"benches/tuono"
|
||||
]
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
# Tuono
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||
<img align="right" src="https://raw.githubusercontent.com/tuono-labs/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
|
||||
a strong focus on usability and performance.**
|
||||
@@ -48,7 +48,7 @@ by Tuono based on the files defined within the `./src/routes` directory.
|
||||
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
|
||||
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
|
||||
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
|
||||
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
|
||||
(check the benchmarks [here](https://github.com/tuono-labs/tuono/tree/main/benches)).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -61,7 +61,7 @@ Then run `cargo install tuono`.
|
||||
To list all the available commands, run `tuono -h`
|
||||
|
||||
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
|
||||
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
||||
[examples](https://github.com/tuono-labs/tuono/tree/main/examples) folder).
|
||||
|
||||
Then, to run the local development environment, install the JS dependencies (`npm install`) and run inside the project folder `tuono dev`
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"reportUnusedDisableDirectives": true,
|
||||
"ignorePatterns": ["**/build", "**/dist"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "import"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/stylistic",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"tsconfigRootDir": ".",
|
||||
"project": true,
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020,
|
||||
},
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
},
|
||||
"react": {
|
||||
"version": "detect",
|
||||
},
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
||||
"@typescript-eslint/no-empty-object-type": "error",
|
||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"error",
|
||||
{ "prefer": "type-imports" },
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
"@typescript-eslint/method-signature-style": ["error", "property"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "typeParameter",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^(T|T[A-Z][A-Za-z]+)$",
|
||||
"match": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-deprecated": "error",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{ "ignoreParameters": true },
|
||||
],
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-cycle": "error",
|
||||
"import/no-duplicates": "off",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-unused-modules": "error",
|
||||
"import/order": [
|
||||
"off",
|
||||
{
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-case-declarations": "error",
|
||||
"no-empty": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
@@ -8,7 +8,7 @@ name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.12.3"
|
||||
tuono_lib = "0.15.0"
|
||||
glob = "0.3.1"
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
|
||||
@@ -3,29 +3,30 @@
|
||||
"description": "The react/rust fullstack framework documentation",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier -u --write --ignore-unknown .",
|
||||
"format:check": "prettier --check --ignore-unknown .",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/code-highlight": "^7.11.2",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"@mantine/code-highlight": "7.14.2",
|
||||
"@mantine/core": "7.14.2",
|
||||
"@mantine/hooks": "7.14.2",
|
||||
"@mdx-js/react": "3.1.0",
|
||||
"@tabler/icons-react": "3.22.0",
|
||||
"clsx": "2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "npm:tuono@0.12.3"
|
||||
"tuono": "npm:tuono@0.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1"
|
||||
"@mdx-js/rollup": "3.1.0",
|
||||
"@types/react": "18.3.13",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"postcss": "8.4.49",
|
||||
"postcss-preset-mantine": "1.17.0",
|
||||
"postcss-simple-vars": "7.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 550 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"polyfills": [
|
||||
{
|
||||
"identifier": "HTMLElement",
|
||||
"import": {
|
||||
"package": "@lit-labs/ssr-dom-shim",
|
||||
"name": "HTMLElement"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifier": "customElements",
|
||||
"import": {
|
||||
"package": "@lit-labs/ssr-dom-shim",
|
||||
"name": "customElements"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +1,19 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ interface Breadcrumb {
|
||||
label: string
|
||||
}
|
||||
interface BreadcrumbsProps {
|
||||
breadcrumbs: Breadcrumb[]
|
||||
breadcrumbs: Array<Breadcrumb>
|
||||
}
|
||||
|
||||
export default function TuonoBreadcrumbs({
|
||||
@@ -28,12 +28,6 @@ export default function TuonoBreadcrumbs({
|
||||
"position": 1,
|
||||
"name": "Tuono - The React/Rust fullstack framework",
|
||||
"item": "https://tuono.dev"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Tuono - Documentation",
|
||||
"item": "https://tuono.dev/documentation"
|
||||
}${breadcrumbs.length > 0 ? ',' : ''}
|
||||
${breadcrumbs
|
||||
.map((br, i) =>
|
||||
@@ -54,13 +48,7 @@ export default function TuonoBreadcrumbs({
|
||||
mb="md"
|
||||
mt="md"
|
||||
>
|
||||
<Button
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
p={5}
|
||||
>
|
||||
<Button href="/" component={Link} variant="subtle" radius="xl" p={5}>
|
||||
<IconBolt />
|
||||
</Button>
|
||||
{breadcrumbs.map((br) => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconEdit } from '@tabler/icons-react'
|
||||
import { useRouter } from 'tuono'
|
||||
|
||||
const GITHUB_URL =
|
||||
'https://github.com/Valerioageno/tuono/tree/main/apps/documentation/src/routes'
|
||||
'https://github.com/tuono-labs/tuono/tree/main/apps/documentation/src/routes'
|
||||
|
||||
export default function EditPage(): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { JSX } from 'react'
|
||||
import type { JSX, HTMLAttributes } from 'react'
|
||||
import { Code } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxCode(
|
||||
props: HTMLAttributes<HTMLPreElement>,
|
||||
): JSX.Element {
|
||||
return <Code {...props} style={{ fontSize: 14 }} />
|
||||
return <Code {...props} style={{ fontSize: 'inherit' }} />
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import type { JSX } from 'react'
|
||||
import { CodeHighlight } from '@mantine/code-highlight'
|
||||
|
||||
import styles from './mdx-pre.module.css'
|
||||
|
||||
interface PreProps {
|
||||
children: any
|
||||
children: {
|
||||
props: {
|
||||
children: string
|
||||
className?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function MdxPre({ children }: PreProps): JSX.Element {
|
||||
return (
|
||||
<CodeHighlight
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { JSX } from 'react'
|
||||
import type { JSX, ReactNode } from 'react'
|
||||
import { MDXProvider } from '@mdx-js/react'
|
||||
|
||||
import MdxLink from './mdx-link'
|
||||
import type { ReactNode } from 'react'
|
||||
import MdxPre from './mdx-pre'
|
||||
import MdxQuote from './mdx-quote'
|
||||
import MdxCode from './mdx-code'
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function MdxTitle(props: TitleProps): JSX.Element {
|
||||
data-order={props.order}
|
||||
mt={20}
|
||||
{...props}
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||
import {
|
||||
IconBrandGithub,
|
||||
IconBook,
|
||||
IconBrandDiscord,
|
||||
} from '@tabler/icons-react'
|
||||
import { Link } from 'tuono'
|
||||
import { Flex, ActionIcon, Group } from '@mantine/core'
|
||||
import { IconBrandGithub, IconBrandDiscord } from '@tabler/icons-react'
|
||||
|
||||
import ThemeBtn from '../theme-btn'
|
||||
|
||||
@@ -13,21 +8,11 @@ export default function Actions(): JSX.Element {
|
||||
return (
|
||||
<Flex gap={8}>
|
||||
<Group gap={8}>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/documentation"
|
||||
visibleFrom="sm"
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
autoContrast
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Check the project on github"
|
||||
href="https://github.com/Valerioageno/tuono"
|
||||
href="https://github.com/tuono-labs/tuono"
|
||||
target="_blank"
|
||||
component="a"
|
||||
>
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { AppShell, Box, Burger, Button, Flex } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
import Actions from './actions'
|
||||
|
||||
interface NavbarProps {
|
||||
opened: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
export default function Navbar({ toggle }: NavbarProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Header p="sm">
|
||||
<Flex justify="space-between">
|
||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||
<Button
|
||||
component={Link}
|
||||
href="/"
|
||||
variant="transparent"
|
||||
p={0}
|
||||
fz={28}
|
||||
hiddenFrom="sm"
|
||||
>
|
||||
Tuono
|
||||
</Button>
|
||||
<Box />
|
||||
<Flex align="center" gap={8}>
|
||||
<Actions />
|
||||
{pathname.startsWith('/documentation') && (
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<Burger onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface SidebarElementBase<T extends string> {
|
||||
type: T
|
||||
}
|
||||
|
||||
type SidebarDivider = SidebarElementBase<'divider'>
|
||||
|
||||
interface SidebarLink extends SidebarElementBase<'element'> {
|
||||
label: string
|
||||
href: string
|
||||
children?: Array<SidebarLink>
|
||||
leftIcon?: ReactNode
|
||||
}
|
||||
|
||||
interface SidebarTitle extends SidebarElementBase<'title'> {
|
||||
label: string
|
||||
}
|
||||
|
||||
type SidebarElement = SidebarDivider | SidebarLink | SidebarTitle
|
||||
|
||||
export const sidebarElements: Array<SidebarElement> = [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Home',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Getting started',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Installation',
|
||||
href: '/documentation/installation',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'How is tuono different?',
|
||||
href: '/documentation/how-is-tuono-different',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Tutorial',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial',
|
||||
label: 'Intro',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/development-setup',
|
||||
label: 'Development setup',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/api-fetching',
|
||||
label: 'API fetching',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/components',
|
||||
label: 'Components',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/dynamic-routes',
|
||||
label: 'Dynamic routes',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/error-handling',
|
||||
label: 'Error handling',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/seo',
|
||||
label: 'SEO and meta tags',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/redirections',
|
||||
label: 'Redirections',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/production',
|
||||
label: 'Production build',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
href: '/documentation/tutorial/conclusion',
|
||||
label: 'Conclusion',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{ type: 'title', label: 'Overview' },
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Routing',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Defining routes',
|
||||
href: '/documentation/routing/defining-routes',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Link and navigation',
|
||||
href: '/documentation/routing/link-and-navigation',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Pages',
|
||||
href: '/documentation/routing/pages',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Loading state',
|
||||
href: '/documentation/routing/loading-state',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Dynamic routes',
|
||||
href: '/documentation/routing/dynamic-routes',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Layouts',
|
||||
href: '/documentation/routing/layouts',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Redirecting',
|
||||
href: '/documentation/routing/redirecting',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Rendering',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Server side rendering',
|
||||
href: '/documentation/rendering/server-side-rendering',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Static site rendering',
|
||||
href: '/documentation/rendering/static-site-rendering',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Styles',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'CSS modules',
|
||||
href: '/documentation/styles/css-modules',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Integrations',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'MDX',
|
||||
href: '/documentation/integrations/mdx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Core concepts',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Multithreading',
|
||||
href: '/documentation/core-concepts/multithreading',
|
||||
},
|
||||
],
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{ type: 'title', label: 'API reference' },
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Components',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Head',
|
||||
href: '/documentation/components/head',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Link',
|
||||
href: '/documentation/components/link',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Hooks',
|
||||
href: '#focus',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'useRouter',
|
||||
href: '/documentation/hooks/use-router',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'CLI',
|
||||
href: '/documentation/cli',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Application state',
|
||||
href: '/documentation/application-state',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'tuono.config.ts',
|
||||
href: '/documentation/configuration',
|
||||
},
|
||||
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Contributing',
|
||||
href: '#focus',
|
||||
leftIcon: '✨',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Guildelines',
|
||||
href: '/documentation/contributing',
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
label: 'Local development',
|
||||
href: '/documentation/contributing/local-development',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -1,6 +1,17 @@
|
||||
.link {
|
||||
border-radius: 8px;
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.25rem;
|
||||
line-height: 1rem;
|
||||
font-weight: 500;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.link span {
|
||||
font-size: 14px !important;
|
||||
color: var(--mantine-color-sidebar-gray);
|
||||
}
|
||||
|
||||
.link:hover span,
|
||||
.active span {
|
||||
color: var(--mantine-color-sidebar-text-hover);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { JSX, ReactNode } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { NavLink, type NavLinkProps } from '@mantine/core'
|
||||
import clsx from 'clsx'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { IconChevronRight } from '@tabler/icons-react'
|
||||
|
||||
@@ -17,24 +17,19 @@ export default function SidebarLink(
|
||||
props: SidebarLinkProps & NavLinkProps,
|
||||
): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
|
||||
|
||||
const isActive = pathname === props.href
|
||||
|
||||
const internalProps = {
|
||||
active: pathname === props.href,
|
||||
className: styles.link,
|
||||
active: isActive,
|
||||
className: clsx(styles.link, isActive && styles.active),
|
||||
rightSection: props.children && (
|
||||
<IconChevronRight
|
||||
size="1.2rem"
|
||||
stroke={1.5}
|
||||
className="mantine-rotate-rtl"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
opened: isOpen,
|
||||
autoContrast: true,
|
||||
...props,
|
||||
}
|
||||
|
||||
@@ -1,108 +1,106 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell } from '@mantine/core'
|
||||
|
||||
import {
|
||||
AppShell,
|
||||
Badge,
|
||||
Flex,
|
||||
Divider,
|
||||
Title,
|
||||
Button,
|
||||
ScrollArea,
|
||||
Text,
|
||||
} from '@mantine/core'
|
||||
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
import { useMediaQuery } from '@mantine/hooks'
|
||||
|
||||
import { sidebarElements } from './config'
|
||||
import SidebarLink from './sidebar-link'
|
||||
|
||||
interface SidebarProps {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
function SidebarHeader({ close }: SidebarProps): JSX.Element {
|
||||
const isSm = useMediaQuery('(min-width: 48em)')
|
||||
return (
|
||||
<AppShell.Section>
|
||||
<Flex mb={20} w="100%" justify="space-between">
|
||||
<Flex
|
||||
gap={10}
|
||||
align="center"
|
||||
w={isSm ? '100%' : 'auto'}
|
||||
justify="center"
|
||||
>
|
||||
<Title order={3}>Tuono</Title>
|
||||
<Badge mt={4} size="xs" variant="outline">
|
||||
Docs
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Button onClick={close} hiddenFrom="sm" variant="transparent" p="0">
|
||||
<IconX />
|
||||
</Button>
|
||||
</Flex>
|
||||
</AppShell.Section>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarElements({ close }: SidebarProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Section component={ScrollArea}>
|
||||
{sidebarElements.map((el, i) => {
|
||||
if (el.type === 'divider') {
|
||||
return <Divider my="md" mx={10} />
|
||||
}
|
||||
if (el.type === 'title') {
|
||||
return (
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
fz={12}
|
||||
pl={12}
|
||||
py={5}
|
||||
children={el.label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (el.children?.length) {
|
||||
return (
|
||||
<SidebarLink
|
||||
href={el.href}
|
||||
label={el.label}
|
||||
key={i}
|
||||
leftSection={el.leftIcon}
|
||||
>
|
||||
{el.children.map((child, index) => (
|
||||
<SidebarLink
|
||||
href={child.href}
|
||||
label={child.label}
|
||||
key={index}
|
||||
onClick={close}
|
||||
/>
|
||||
))}
|
||||
</SidebarLink>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SidebarLink
|
||||
href={el.href}
|
||||
label={el.label}
|
||||
onClick={close}
|
||||
key={i}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</AppShell.Section>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Sidebar({ close }: SidebarProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<SidebarLink
|
||||
href="/documentation"
|
||||
label="Getting started"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/installation"
|
||||
label="Installation"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial"
|
||||
label="Tutorial"
|
||||
defaultOpened
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/development-setup"
|
||||
label="Development setup"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/api-fetching"
|
||||
label="API fetching"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/components"
|
||||
label="Components"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/dynamic-routes"
|
||||
label="Dynamic routes"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/error-handling"
|
||||
label="Error handling"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/seo"
|
||||
label="SEO and meta tags"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/redirections"
|
||||
label="Server redirection"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/production"
|
||||
label="Production build"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/conclusion"
|
||||
label="Conclusion"
|
||||
onClick={close}
|
||||
/>
|
||||
</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"
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/routing/intro"
|
||||
label="Project structure"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
|
||||
<SidebarLink
|
||||
label="Contributing"
|
||||
href="/documentation/contributing"
|
||||
leftSection="✨"
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/contributing/local-development"
|
||||
label="Local development"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
<SidebarHeader close={close} />
|
||||
<SidebarElements close={close} />
|
||||
</AppShell.Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export interface Heading {
|
||||
getNode: () => HTMLHeadingElement
|
||||
}
|
||||
|
||||
function getHeadingsData(headings: HTMLHeadingElement[]): Heading[] {
|
||||
const result: Heading[] = []
|
||||
function getHeadingsData(headings: Array<HTMLHeadingElement>): Array<Heading> {
|
||||
const result: Array<Heading> = []
|
||||
|
||||
for (const heading of headings) {
|
||||
if (heading.id) {
|
||||
@@ -26,7 +26,7 @@ function getHeadingsData(headings: HTMLHeadingElement[]): Heading[] {
|
||||
return result
|
||||
}
|
||||
|
||||
export function getHeadings(): Heading[] {
|
||||
export function getHeadings(): Array<Heading> {
|
||||
const root = document.getElementById('mdx-root')
|
||||
console.log(root)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouter, Link } from 'tuono'
|
||||
import { IconList } from '@tabler/icons-react'
|
||||
import { Box, rem, ScrollArea, Text } from '@mantine/core'
|
||||
|
||||
import { getHeadings, type Heading } from './get-headings'
|
||||
import classes from './table-of-content.module.css'
|
||||
|
||||
@@ -13,7 +14,7 @@ interface TableOfContentsProps {
|
||||
withTabs: boolean
|
||||
}
|
||||
|
||||
function getActiveElement(rects: DOMRect[]): number {
|
||||
function getActiveElement(rects: Array<DOMRect>): number {
|
||||
if (rects.length === 0) {
|
||||
return -1
|
||||
}
|
||||
@@ -39,8 +40,8 @@ export function TableOfContents({
|
||||
withTabs,
|
||||
}: TableOfContentsProps): JSX.Element | null {
|
||||
const [active, setActive] = useState(0)
|
||||
const [headings, setHeadings] = useState<Heading[]>([])
|
||||
const headingsRef = useRef<Heading[]>([])
|
||||
const [headings, setHeadings] = useState<Array<Heading>>([])
|
||||
const headingsRef = useRef<Array<Heading>>([])
|
||||
const router = useRouter()
|
||||
|
||||
const filteredHeadings = headings.filter((heading) => heading.depth > 1)
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
// declaration.d.ts
|
||||
declare module '*.css'
|
||||
declare module '*.css' {
|
||||
const CSSModule: Record<string, string>
|
||||
export default CSSModule
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
createTheme,
|
||||
MantineProvider,
|
||||
AppShell,
|
||||
Container,
|
||||
} from '@mantine/core'
|
||||
import type { CSSVariablesResolver } from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { Head, useRouter } from 'tuono'
|
||||
import Navbar from '../components/navbar'
|
||||
import { Head } from 'tuono'
|
||||
|
||||
import EditPage from '@/components/edit-page'
|
||||
import MdxProvider from '@/components/mdx-provider'
|
||||
import Navbar from '@/components/navbar'
|
||||
import Sidebar from '@/components/sidebar'
|
||||
|
||||
import '@mantine/core/styles.css'
|
||||
import '@mantine/code-highlight/styles.css'
|
||||
|
||||
import Sidebar from '../components/sidebar'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
@@ -21,7 +26,7 @@ interface RootRouteProps {
|
||||
const theme = createTheme({
|
||||
primaryColor: 'violet',
|
||||
primaryShade: { light: 6, dark: 9 },
|
||||
fontFamily: 'Roboto',
|
||||
fontFamily: 'Inter',
|
||||
respectReducedMotion: true,
|
||||
radius: {
|
||||
xs: '8px',
|
||||
@@ -56,13 +61,31 @@ const theme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
other: {
|
||||
sidebarGrayLight: '#495057',
|
||||
sidebarGrayDark: '#adb5bd',
|
||||
sidebarTextHoverLight: '#212529',
|
||||
sidebarTextHoverDark: '#f8f9fa',
|
||||
},
|
||||
})
|
||||
|
||||
const resolver: CSSVariablesResolver = (th) => ({
|
||||
variables: {},
|
||||
light: {
|
||||
'--mantine-color-sidebar-gray': th.other.sidebarGrayLight as string,
|
||||
'--mantine-color-sidebar-text-hover': th.other
|
||||
.sidebarTextHoverLight as string,
|
||||
},
|
||||
dark: {
|
||||
'--mantine-color-sidebar-gray': th.other.sidebarGrayDark as string,
|
||||
'--mantine-color-sidebar-text-hover': th.other
|
||||
.sidebarTextHoverDark as string,
|
||||
},
|
||||
})
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -88,8 +111,9 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<ColorSchemeScript />
|
||||
<MantineProvider theme={theme}>
|
||||
<MantineProvider theme={theme} cssVariablesResolver={resolver}>
|
||||
<AppShell
|
||||
layout="alt"
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
@@ -97,9 +121,14 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||
{children}
|
||||
<Navbar toggle={toggle} />
|
||||
<Sidebar close={toggle} />
|
||||
<AppShell.Main>
|
||||
<Container id="mdx-root" component="article" size="md" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
import { AppShell, Container } from '@mantine/core'
|
||||
|
||||
import MdxProvider from '../../components/mdx-provider'
|
||||
import EditPage from '../../components/edit-page'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Main>
|
||||
<Container id="mdx-root" component="article" size="md" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Application state"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../components/meta-tags'
|
||||
description="Learn how to add features to your Tuono application"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - CLI"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../components/meta-tags'
|
||||
description="Tuono is the CLI that provides all the needed commands to handle the full-stack project."
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Head"
|
||||
canonical="https://tuono.dev/documentation/components/head"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Components' }]} />
|
||||
|
||||
# Head
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Link"
|
||||
canonical="https://tuono.dev/documentation/components/link"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Components' }]} />
|
||||
|
||||
# Link
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - tuono.config.ts"
|
||||
canonical="https://tuono.dev/documentation/configuration"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Configuration' }]} />
|
||||
|
||||
# Configuration
|
||||
|
||||
TODO
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Contributing"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../components/meta-tags'
|
||||
description="The project is massive - if you like it, do consider contributing!"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Contributing"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="The project is massive - if you like it, do consider contributing!"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
|
||||
|
||||
@@ -28,11 +28,10 @@ The `tuono` project can mostly be split into the following subdomains:
|
||||
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__`.
|
||||
We are currently managing the tasks' prioritization in the [Road to V1](https://github.com/orgs/tuono-labs/projects/2) project.
|
||||
You can pick any task that you see in the `ready` column that is not already assigned.
|
||||
|
||||
If you want to contribute on something that is in the `backlog` column, or it hasn't been scoped yet, please drop a message in the [#general](https://discord.com/invite/khQzPa654B) discord channel so we can all have visibility of it.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Contributing - Local development"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Contribute to Tuono. Learn here how to setup the repository for local development"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Multithreading"
|
||||
canonical="https://tuono.dev/documentation/core-concepts/multithreading"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Core concepts' }]} />
|
||||
|
||||
# Multithreading
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - useRouter"
|
||||
canonical="https://tuono.dev/documentation/hooks/use-router"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Hooks' }]} />
|
||||
|
||||
# useRouter
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - tuono.config.ts"
|
||||
canonical="https://tuono.dev/documentation/how-is-tuono-different"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'How is tuono different?' }]} />
|
||||
|
||||
# How is tuono different?
|
||||
|
||||
TODO
|
||||
@@ -1,6 +1,6 @@
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Installation"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - MDX"
|
||||
canonical="https://tuono.dev/documentation/integrations/mdx"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Integrations' }]} />
|
||||
|
||||
# MDX
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Server side rendering"
|
||||
canonical="https://tuono.dev/documentation/rendering/server-side-rendering"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Rendering' }]} />
|
||||
|
||||
# SSR
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Static site rendering"
|
||||
canonical="https://tuono.dev/documentation/rendering/static-site-rendering"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Rendering' }]} />
|
||||
|
||||
# Static Site Rendering
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,101 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/defining-routes"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Defining routes' }]} />
|
||||
|
||||
# Defining routes
|
||||
|
||||
Tuono has a file-system based router built on the concept of routes.
|
||||
|
||||
When a file is added to the `src/routes` directory, it's automatically available as a route.
|
||||
|
||||
In Tuono, a page is a [React Component](https://react.dev/learn/your-first-component) exported from a `.tsx` file in the `src/routes` directory.
|
||||
Each page is associated with a route based on its file name.
|
||||
|
||||
Every page is rendered server side by default.
|
||||
|
||||
Example:
|
||||
If you create `src/routes/about.tsx` that exports a React component like:
|
||||
|
||||
```
|
||||
export default function About() {
|
||||
return <div>About</div>
|
||||
}
|
||||
```
|
||||
|
||||
The `About` component will be rendered when loading `/about` route.
|
||||
|
||||
## Index routes
|
||||
|
||||
The router will automatically route files named `index` to the root of the directory.
|
||||
|
||||
- `src/routes/index.tsx` → `/`
|
||||
- `src/routes/blog/index.tsx`→ `/blog`
|
||||
|
||||
## Nested routes
|
||||
|
||||
You can also create a nested folder structure, and files will still be routed in the same way.
|
||||
|
||||
- `src/routes/blog/first-post.tsx` → `/blog/first-post`
|
||||
- `src/routes/dashboard/settings/username.tsx` → `/dashboard/settings/username`
|
||||
|
||||
## Pages with Dynamic Routes
|
||||
|
||||
Tuono supports pages with dynamic routes segments.
|
||||
For example, if you create a file called `src/routes/posts/[id].tsx`,
|
||||
then it will be accessible at `posts/1`, `posts/2`, etc.
|
||||
|
||||
## The Root route (Layout components)
|
||||
|
||||
Tuono allows you to have a layout component to wrap all the routes included within the same folder.
|
||||
To define such component you will have to create a `__root.tsx` file (is allowed only a single `__root` file per folder).
|
||||
|
||||
This file won't generate any route.
|
||||
|
||||
### Layout component for all routes
|
||||
|
||||
A file created in this location will wrap all project route.
|
||||
|
||||
```tsx
|
||||
// src/routes/__root.tsx
|
||||
export default function RootLayout({ children }) {
|
||||
return <main>{children}</main>
|
||||
}
|
||||
```
|
||||
|
||||
### Layout component for specific routes
|
||||
|
||||
A file created in this location will wrap only the routes defined within the `src/routes/posts` folder.
|
||||
|
||||
```tsx
|
||||
// src/routes/posts/__root.tsx
|
||||
export default function PostRootLayout({ children }) {
|
||||
return <article>{children}</article>
|
||||
}
|
||||
```
|
||||
|
||||
Referring to the two examples above consider that:
|
||||
|
||||
- `/posts/examples-post` → will be wrapped by:
|
||||
|
||||
```tsx
|
||||
<RootLayout>
|
||||
<PostRootLayout>
|
||||
<ExamplePost />
|
||||
</PostRootLayout>
|
||||
</RootLayout>
|
||||
```
|
||||
|
||||
- `/about` → will be wrapped by:
|
||||
|
||||
```tsx
|
||||
<RootLayout>
|
||||
<About />
|
||||
</RootLayout>
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/dynamic-routes"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Dynamic routes' }]} />
|
||||
|
||||
# Loading state
|
||||
|
||||
Todo
|
||||
@@ -1,11 +1,11 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/intro"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Routing', href: '/documentation/routing' },
|
||||
{ label: 'Project structure' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Project structure
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/layouts"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Layouts' }]} />
|
||||
|
||||
# Layouts
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,75 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/link-and-navigation"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Link and navigation' }]} />
|
||||
|
||||
# Link and navigation
|
||||
|
||||
The Tuono router facilitates client-side route transitions between pages, enhancing the user experience by:
|
||||
|
||||
- Enabling smooth navigation in single-page applications (SPAs)
|
||||
- Preventing page reloads during site navigation
|
||||
|
||||
How? Using the `Link` component provided from Tuono exports:
|
||||
|
||||
```tsx
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/">Home</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about">About Us</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/blog/hello-world">Blog Post</Link>
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The example above uses multiple links. Each one maps a path (`href`) to a known route:
|
||||
|
||||
- `/` → `src/routes/index.tsx`
|
||||
- `/about` → `src/routes/about.tsx`
|
||||
- `/blog/hello-world` → `src/routes/blog/[slug].tsx`
|
||||
|
||||
> Additional considerations:
|
||||
>
|
||||
> - Any `<Link />` in the viewport is prefetched by default when it appears within the viewport.
|
||||
> - The corresponding data for server-rendered routes is fetched only when the `<Link />` is clicked.
|
||||
|
||||
## The `useRouter` hook
|
||||
|
||||
The Link component is not suitable for programmatic navigation.
|
||||
To handle this, the library provides the `useRouter` hook.
|
||||
|
||||
For example, after a user submits a form and the API request succeeds, they can be redirected to another page.
|
||||
|
||||
```tsx
|
||||
import { useRouter } from 'tuono'
|
||||
|
||||
export default function GoToAboutPage() {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<ProfileForm
|
||||
onUpdateSuccess={() => {
|
||||
router.push('/profile')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> For more information about this hook visit the [dedicated API reference page](/documentation/hooks/use-router).
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/loading-state"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Loading state' }]} />
|
||||
|
||||
# Loading state
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/pages"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Pages' }]} />
|
||||
|
||||
# Pages
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/redirecting"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Redirecting' }]} />
|
||||
|
||||
# Redirecting
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,14 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - CSS Modules"
|
||||
canonical="https://tuono.dev/documentation/styles/css-modules"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Styles' }]} />
|
||||
|
||||
# CSS Modules
|
||||
|
||||
TODO
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - API fetching"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to fetch remote APIs on the backend"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Components"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to manage the components in a Tuono codebase"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Conclusion"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="You just created a multi thread full stack application with Rust and React"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Development setup"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to setup a new Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Dynamic routes"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to catch more endpoint with the same route loading dynamic data"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Error handling"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to handle the server side error on your Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Tutorial"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="This tutorial is meant to give you a sneak peek at the framework and is intended to evolve during the development - be sure to have installed the latest version"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Building for production"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to build the production project and run the optimize version"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Redirection"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to redirect to different route from the server"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MetaTags from '../../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - SEO and meta tags"
|
||||
@@ -6,7 +6,7 @@ import MetaTags from '../../../components/meta-tags'
|
||||
description="Learn how to handle meaningful meta tags for your Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import MetaTags from '../../components/meta-tags'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Documentation"
|
||||
canonical="https://tuono.dev/documentation"
|
||||
canonical="https://tuono.dev"
|
||||
description="Tuono is a full-stack framework for building React applications using Rust as the backend."
|
||||
/>
|
||||
|
||||
import Breadcrumbs from '../../components/breadcrumbs'
|
||||
import Breadcrumbs from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs />
|
||||
|
||||
@@ -36,7 +36,7 @@ Tuono addresses the following two limits:
|
||||
|
||||
#### NodeJs/Deno/Bun are the only runtimes that allow a React app to be server-side rendered (SSR)? (no)
|
||||
|
||||
Tuono is the first full-stack React framework with the server side written in Rust (check the [ssr-rs](https://github.com/Valerioageno/ssr-rs) crate).
|
||||
Tuono is the first full-stack React framework with the server side written in Rust (check the [ssr-rs](https://github.com/tuono-labs/ssr-rs) crate).
|
||||
Because of this, Tuono is extremely fast, and the requests are handled by a multithreaded Rust server.
|
||||
React is still React - it is just superpowered.
|
||||
|
||||
@@ -51,4 +51,4 @@ The routing is handled by Tuono based on the files defined within the `./src/rou
|
||||
The Tuono API tries to stick as much as possible to the Next.js one (or at least takes a huge inspiration
|
||||
from it). The major difference is the backend system. While Next.js relies entirely on Node/Deno/Bun,
|
||||
Tuono runs the server without any intermediary runtime. This enables impressive performance improvements
|
||||
(check the benchmarks [here](https://github.com/Valerioageno/tuono/tree/main/benches)).
|
||||
(check the benchmarks [here](https://github.com/tuono-labs/tuono/tree/main/benches)).
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import Hero from '../components/hero'
|
||||
import MetaTags from '../components/meta-tags'
|
||||
|
||||
export default function IndexPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<MetaTags
|
||||
title="Tuono - The React/Rust full-stack framework"
|
||||
canonical="https://tuono.dev"
|
||||
description="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
|
||||
/>
|
||||
<Hero />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use tuono_lib::reqwest::StatusCode;
|
||||
use tuono_lib::{Request, Response};
|
||||
use tuono_lib::axum::http::{header, HeaderMap};
|
||||
use glob::glob;
|
||||
use time::OffsetDateTime;
|
||||
use tuono_lib::axum::http::{header, HeaderMap, StatusCode};
|
||||
use tuono_lib::{Request, Response};
|
||||
|
||||
const FILE_TO_EXCLUDE: [&str; 2] = ["sitemap.xml", "__root"];
|
||||
|
||||
@@ -14,11 +13,17 @@ const SITEMAP: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
fn load_routes() -> Vec<String> {
|
||||
let mut paths: Vec<String> = vec![];
|
||||
|
||||
for entry in glob("./src/routes/**/*").expect("Failed to glob src/routes folder").flatten() {
|
||||
for entry in glob("./src/routes/**/*")
|
||||
.expect("Failed to glob src/routes folder")
|
||||
.flatten()
|
||||
{
|
||||
if !entry.is_dir() {
|
||||
let path = clean_path(format!("/{}", entry.to_string_lossy()));
|
||||
|
||||
if !FILE_TO_EXCLUDE.iter().any(|exclude| path.ends_with(exclude)) {
|
||||
if !FILE_TO_EXCLUDE
|
||||
.iter()
|
||||
.any(|exclude| path.ends_with(exclude))
|
||||
{
|
||||
paths.push(path)
|
||||
}
|
||||
}
|
||||
@@ -51,11 +56,16 @@ async fn generate_sitemap(_req: Request) -> Response {
|
||||
url.pop();
|
||||
}
|
||||
|
||||
sitemaps.push_str(
|
||||
&format!(r#"<url><loc>{}</loc><lastmod>{}</lastmod></url>"#,url, OffsetDateTime::now_utc().date())
|
||||
)
|
||||
sitemaps.push_str(&format!(
|
||||
r#"<url><loc>{}</loc><lastmod>{}</lastmod></url>"#,
|
||||
url,
|
||||
OffsetDateTime::now_utc().date()
|
||||
))
|
||||
}
|
||||
|
||||
Response::Custom((StatusCode::OK, headers, SITEMAP.replace("[PLACEHOLDER]", &sitemaps)))
|
||||
Response::Custom((
|
||||
StatusCode::OK,
|
||||
headers,
|
||||
SITEMAP.replace("[PLACEHOLDER]", &sitemaps),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// JavaScript Support
|
||||
"allowJs": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "tuono.config.ts", "postcss.config.js"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { TuonoConfig } from 'tuono/config'
|
||||
import mdx from '@mdx-js/rollup'
|
||||
|
||||
const config: TuonoConfig = {
|
||||
vite: {
|
||||
alias: {
|
||||
'@': 'src',
|
||||
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@mdx-js/react'],
|
||||
},
|
||||
plugins: [
|
||||
{ enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react' }) },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.14.3"
|
||||
version = "0.16.2"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -26,6 +26,7 @@ miette = "7.2.0"
|
||||
watchexec-signals = "4.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
toml = "0.8.19"
|
||||
watchexec-supervisor = "3.0.0"
|
||||
glob = "0.3.1"
|
||||
regex = "1.10.4"
|
||||
@@ -33,3 +34,8 @@ reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
||||
serde_json = "1.0"
|
||||
fs_extra = "1.3.0"
|
||||
http = "1.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.14.0"
|
||||
assert_cmd = "2.0.16"
|
||||
serial_test = "0.4.0"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use glob::glob;
|
||||
use glob::GlobError;
|
||||
use http::Method;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
@@ -10,6 +12,7 @@ use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use toml::{Table, Value};
|
||||
|
||||
use crate::route::Route;
|
||||
|
||||
@@ -21,19 +24,26 @@ const ROUTES_FOLDER_PATH: &str = "\\src\\routes";
|
||||
#[cfg(target_os = "windows")]
|
||||
const BUILD_JS_SCRIPT: &str = ".\\node_modules\\.bin\\tuono-build-prod.cmd";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.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";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const BUILD_TUONO_CONFIG: &str = "./node_modules/.bin/tuono-build-config";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
pub route_map: HashMap<String, Route>,
|
||||
pub base_path: PathBuf,
|
||||
pub has_app_state: bool,
|
||||
pub is_api_only_mode: bool,
|
||||
}
|
||||
|
||||
fn has_app_state(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/app.rs"))?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
@@ -41,6 +51,48 @@ fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||
Ok(contents.contains("pub fn main"))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct CargoToml {
|
||||
dependencies: Table,
|
||||
}
|
||||
|
||||
fn is_api_only_mode(base_path: &PathBuf) -> bool {
|
||||
if let Ok(file) = File::open(base_path.join("Cargo.toml")) {
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
if let Ok(_) = buf_reader.read_to_string(&mut contents) {
|
||||
let cargo_toml = toml::from_str::<CargoToml>(&contents).unwrap_or_else(|_| {
|
||||
eprintln!("Failed to parse Cargo.toml content");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
match cargo_toml.dependencies.get("tuono_lib") {
|
||||
Some(tuono_lib) => match tuono_lib {
|
||||
Value::String(_) => return false,
|
||||
Value::Table(elements) => match elements.get("features") {
|
||||
Some(features) => match features {
|
||||
Value::Array(ftrs) => {
|
||||
return ftrs.contains(&Value::String("api".into()))
|
||||
}
|
||||
_ => return false,
|
||||
},
|
||||
None => return false,
|
||||
},
|
||||
_ => return false,
|
||||
},
|
||||
None => {
|
||||
eprintln!("tuono_lib dependency not found in Cargo.toml");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("Failed to read Cargo.toml file");
|
||||
std::process::exit(1);
|
||||
};
|
||||
eprintln!("Cargo.toml file not found");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let base_path = std::env::current_dir().expect("Failed to read current_dir");
|
||||
@@ -48,7 +100,8 @@ impl App {
|
||||
let mut app = App {
|
||||
route_map: HashMap::new(),
|
||||
base_path: base_path.clone(),
|
||||
has_app_state: has_app_state(base_path).unwrap_or(false),
|
||||
has_app_state: has_app_state(&base_path).unwrap_or(false),
|
||||
is_api_only_mode: is_api_only_mode(&base_path),
|
||||
};
|
||||
|
||||
app.collect_routes();
|
||||
@@ -112,7 +165,7 @@ impl App {
|
||||
|
||||
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);
|
||||
let mut route = Route::new(path, self.is_api_only_mode);
|
||||
route.update_axum_info();
|
||||
route_map.insert(route);
|
||||
} else {
|
||||
@@ -122,7 +175,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||
let route = Route::new(path);
|
||||
let route = Route::new(path, self.is_api_only_mode);
|
||||
route_map.insert(route);
|
||||
}
|
||||
}
|
||||
@@ -147,6 +200,13 @@ impl App {
|
||||
.expect("Failed to run the rust server")
|
||||
}
|
||||
|
||||
pub fn build_tuono_config(&self) -> Result<std::process::Output, std::io::Error> {
|
||||
Command::new(BUILD_TUONO_CONFIG)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
}
|
||||
pub fn get_used_http_methods(&self) -> HashSet<Method> {
|
||||
let mut acc = HashSet::new();
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ enum Actions {
|
||||
#[arg(short, long = "static")]
|
||||
/// Statically generate the website HTML
|
||||
ssg: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
/// Prevent to export the js assets
|
||||
no_js_emit: bool,
|
||||
},
|
||||
/// Scaffold a new project
|
||||
New {
|
||||
@@ -42,12 +46,12 @@ struct Args {
|
||||
action: Actions,
|
||||
}
|
||||
|
||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<App> {
|
||||
check_tuono_folder()?;
|
||||
bundle_axum_source(mode)?;
|
||||
let app = bundle_axum_source(mode)?;
|
||||
create_client_entry_files()?;
|
||||
|
||||
Ok(())
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn check_ports(mode: Mode) {
|
||||
@@ -83,13 +87,21 @@ pub fn app() -> std::io::Result<()> {
|
||||
Actions::Dev => {
|
||||
check_ports(Mode::Dev);
|
||||
|
||||
init_tuono_folder(Mode::Dev)?;
|
||||
let app = init_tuono_folder(Mode::Dev)?;
|
||||
if !app.is_api_only_mode {
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
}
|
||||
|
||||
watch::watch().unwrap();
|
||||
watch::watch(app.is_api_only_mode).unwrap();
|
||||
}
|
||||
Actions::Build { ssg } => {
|
||||
init_tuono_folder(Mode::Prod)?;
|
||||
let app = App::new();
|
||||
Actions::Build { ssg, no_js_emit } => {
|
||||
let app = init_tuono_folder(Mode::Prod)?;
|
||||
|
||||
if no_js_emit || app.is_api_only_mode {
|
||||
println!("Rust build successfully finished");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ssg && app.has_dynamic_routes() {
|
||||
// TODO: allow dynamic routes static generation
|
||||
@@ -97,6 +109,9 @@ pub fn app() -> std::io::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
|
||||
app.build_react_prod();
|
||||
|
||||
if ssg {
|
||||
@@ -120,6 +135,8 @@ pub fn app() -> std::io::Result<()> {
|
||||
)
|
||||
.expect("Failed to clone assets into static output folder");
|
||||
|
||||
// the process is killed below so we don't really need to use wait() function
|
||||
#[allow(clippy::zombie_processes)]
|
||||
let mut rust_server = app.run_rust_server();
|
||||
|
||||
let reqwest = reqwest::blocking::Client::builder()
|
||||
@@ -131,7 +148,8 @@ pub fn app() -> std::io::Result<()> {
|
||||
let mut is_server_ready = false;
|
||||
|
||||
while !is_server_ready {
|
||||
if reqwest.get("http://localhost:3000").send().is_ok() {
|
||||
let server_url = format!("http://localhost:{}", TUONO_PORT);
|
||||
if reqwest.get(server_url).send().is_ok() {
|
||||
is_server_ready = true
|
||||
}
|
||||
// TODO: add maximum tries
|
||||
|
||||
@@ -22,12 +22,12 @@ pub struct AxumInfo {
|
||||
}
|
||||
|
||||
impl AxumInfo {
|
||||
pub fn new(path: String) -> Self {
|
||||
pub fn new(route: &Route) -> Self {
|
||||
// Remove first slash
|
||||
let mut module = path.chars();
|
||||
let mut module = route.path.chars();
|
||||
module.next();
|
||||
|
||||
let axum_route = path.replace("/index", "");
|
||||
let axum_route = route.path.replace("/index", "");
|
||||
|
||||
let module_import = module
|
||||
.as_str()
|
||||
@@ -44,7 +44,7 @@ impl AxumInfo {
|
||||
};
|
||||
}
|
||||
|
||||
if has_dynamic_path(&path) {
|
||||
if route.is_dynamic {
|
||||
return AxumInfo {
|
||||
module_import: module
|
||||
.as_str()
|
||||
@@ -52,8 +52,12 @@ impl AxumInfo {
|
||||
.replace('/', "_")
|
||||
.replace('-', "_hyphen_")
|
||||
.replace('[', "dyn_")
|
||||
.replace("...", "_catch_all_")
|
||||
.replace(']', ""),
|
||||
axum_route: axum_route
|
||||
.replace("[...", "*")
|
||||
.replace('[', ":")
|
||||
.replace(']', ""),
|
||||
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,8 +97,8 @@ pub struct ApiData {
|
||||
}
|
||||
|
||||
impl ApiData {
|
||||
pub fn new(path: &String) -> Option<Self> {
|
||||
if !path.starts_with("/api/") {
|
||||
pub fn new(path: &String, is_api_only_mode: bool) -> Option<Self> {
|
||||
if !path.starts_with("/api/") && !is_api_only_mode {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -120,12 +124,12 @@ pub struct Route {
|
||||
}
|
||||
|
||||
impl Route {
|
||||
pub fn new(cleaned_path: String) -> Self {
|
||||
pub fn new(cleaned_path: String, is_api_only_mode: bool) -> Self {
|
||||
Route {
|
||||
path: cleaned_path.clone(),
|
||||
axum_info: None,
|
||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path, is_api_only_mode),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +138,7 @@ impl Route {
|
||||
}
|
||||
|
||||
pub fn update_axum_info(&mut self) {
|
||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||
self.axum_info = Some(AxumInfo::new(self))
|
||||
}
|
||||
|
||||
pub fn save_ssg_file(&self, reqwest: &Client) {
|
||||
@@ -159,8 +163,7 @@ impl Route {
|
||||
|
||||
// Saving also the server response
|
||||
if self.axum_info.is_some() {
|
||||
let data_file_path =
|
||||
PathBuf::from(&format!("out/static/__tuono/data{}/data.json", path));
|
||||
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}"));
|
||||
|
||||
let data_parent_dir = data_file_path.parent().unwrap();
|
||||
|
||||
@@ -169,11 +172,11 @@ impl Route {
|
||||
.expect("Failed to create data parent directories");
|
||||
}
|
||||
|
||||
let base = Url::parse("http://localhost:3000/__tuono/data/data.json").unwrap();
|
||||
let base = Url::parse("http://localhost:3000/__tuono/data").unwrap();
|
||||
|
||||
let path = if path == "/" { "" } else { path };
|
||||
|
||||
let pathname = &format!("/__tuono/data{path}/data.json");
|
||||
let pathname = &format!("/__tuono/data{path}");
|
||||
|
||||
let url = base
|
||||
.join(pathname)
|
||||
@@ -229,12 +232,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_correctly_create_the_axum_infos() {
|
||||
let info = AxumInfo::new("/index".to_string());
|
||||
let info = AxumInfo::new(&Route::new("/index".to_string(), false));
|
||||
|
||||
assert_eq!(info.axum_route, "/");
|
||||
assert_eq!(info.module_import, "index");
|
||||
|
||||
let dyn_info = AxumInfo::new("/[posts]".to_string());
|
||||
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string(), false));
|
||||
|
||||
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||
@@ -253,7 +256,7 @@ mod tests {
|
||||
];
|
||||
|
||||
for (path, html) in routes {
|
||||
let route = Route::new(path.to_string());
|
||||
let route = Route::new(path.to_string(), false);
|
||||
|
||||
assert_eq!(route.output_file_path(), PathBuf::from(html))
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::io::{self, prelude::*};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const GITHUB_TUONO_REPO_URL: &str =
|
||||
"https://api.github.com/repos/Valerioageno/tuono/git/trees/main?recursive=1";
|
||||
"https://api.github.com/repos/tuono-labs/tuono/git/trees/main?recursive=1";
|
||||
|
||||
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/Valerioageno/tuono/main/";
|
||||
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/tuono-labs/tuono/main/";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum GithubFileType {
|
||||
@@ -64,7 +64,7 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
|
||||
|
||||
if new_project_files.is_empty() {
|
||||
eprintln!("Error: Template '{template}' not found");
|
||||
println!("Hint: you can view the available templates at https://github.com/Valerioageno/tuono/tree/main/examples");
|
||||
println!("Hint: you can view the available templates at https://github.com/tuono-labs/tuono/tree/main/examples");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,10 +86,9 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
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))"#
|
||||
r#".route("/__tuono/data{axum_route}", get({module_import}::api))"#
|
||||
));
|
||||
} else {
|
||||
for method in route.api_data.as_ref().unwrap().methods.clone() {
|
||||
@@ -123,7 +122,7 @@ fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
route_declarations
|
||||
}
|
||||
|
||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<App> {
|
||||
let base_path = std::env::current_dir().unwrap();
|
||||
|
||||
let app = App::new();
|
||||
@@ -132,7 +131,7 @@ pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
||||
|
||||
create_main_file(&base_path, &bundled_file);
|
||||
|
||||
Ok(())
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||
@@ -236,7 +235,7 @@ mod tests {
|
||||
fn should_load_the_axum_get_function() {
|
||||
let mut source_builder = App::new();
|
||||
|
||||
let mut route = Route::new(String::from("index.tsx"));
|
||||
let mut route = Route::new(String::from("index.tsx"), false);
|
||||
route.update_axum_info();
|
||||
|
||||
source_builder
|
||||
|
||||
@@ -52,20 +52,7 @@ fn build_react_ssr_src() -> Job {
|
||||
.0
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn watch() -> Result<()> {
|
||||
watch_react_src().start().await;
|
||||
|
||||
let run_server = build_rust_src();
|
||||
|
||||
let build_ssr_bundle = build_react_ssr_src();
|
||||
|
||||
build_ssr_bundle.start().await;
|
||||
|
||||
run_server.start().await;
|
||||
|
||||
build_ssr_bundle.to_wait().await;
|
||||
|
||||
async fn watch_exec_handler(is_api_only_mode: bool, rust_server: Job, vite_server: Option<Job>) {
|
||||
let wx = Watchexec::new(move |mut action| {
|
||||
let mut should_reload_ssr_bundle = false;
|
||||
let mut should_reload_rust_server = false;
|
||||
@@ -78,7 +65,7 @@ pub async fn watch() -> Result<()> {
|
||||
}
|
||||
|
||||
// Either tsx, jsx and mdx
|
||||
if file_path.ends_with("sx") || file_path.ends_with("mdx") {
|
||||
if !is_api_only_mode && (file_path.ends_with("sx") || file_path.ends_with("mdx")) {
|
||||
should_reload_ssr_bundle = true
|
||||
}
|
||||
}
|
||||
@@ -86,14 +73,15 @@ pub async fn watch() -> Result<()> {
|
||||
|
||||
if should_reload_rust_server {
|
||||
println!(" Reloading...");
|
||||
run_server.stop();
|
||||
rust_server.stop();
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
|
||||
run_server.start();
|
||||
rust_server.start();
|
||||
}
|
||||
|
||||
if should_reload_ssr_bundle {
|
||||
build_ssr_bundle.stop();
|
||||
build_ssr_bundle.start();
|
||||
if should_reload_ssr_bundle && !is_api_only_mode {
|
||||
let vite = vite_server.as_ref().unwrap();
|
||||
vite.stop();
|
||||
vite.start();
|
||||
}
|
||||
|
||||
// if Ctrl-C is received, quit
|
||||
@@ -102,11 +90,31 @@ pub async fn watch() -> Result<()> {
|
||||
}
|
||||
|
||||
action
|
||||
})?;
|
||||
})
|
||||
.expect("Failed to create WatchExec hanlder");
|
||||
|
||||
// watch the current directory
|
||||
wx.config.pathset(["./src"]);
|
||||
|
||||
let _ = wx.main().await.into_diagnostic()?;
|
||||
let _ = wx.main().await.into_diagnostic().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn watch(is_api_only_mode: bool) -> Result<()> {
|
||||
let run_server = build_rust_src();
|
||||
|
||||
if !is_api_only_mode {
|
||||
watch_react_src().start().await;
|
||||
let build_ssr_bundle = build_react_ssr_src();
|
||||
build_ssr_bundle.start().await;
|
||||
run_server.start().await;
|
||||
build_ssr_bundle.to_wait().await;
|
||||
watch_exec_handler(is_api_only_mode, run_server, Some(build_ssr_bundle)).await;
|
||||
} else {
|
||||
run_server.start().await;
|
||||
|
||||
watch_exec_handler(is_api_only_mode, run_server, None).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
mod utils;
|
||||
use assert_cmd::Command;
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use utils::TempTuonoProject;
|
||||
|
||||
const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]";
|
||||
const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]";
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_the_index_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_route("./src/routes/index.rs");
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/index.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod index;"));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/", get(index::route)).route("/__tuono/data/", get(index::api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_an_api_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_api("./src/routes/api/health_check.rs", POST_API_FILE);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
dbg!(&temp_main_rs_content);
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_multiple_api_for_the_same_file() {
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_api(
|
||||
"./src/routes/api/health_check.rs",
|
||||
&format!("{POST_API_FILE}{GET_API_FILE}"),
|
||||
);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
|
||||
));
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", get(api_health_check::get__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_catch_all_routes() {
|
||||
let temp_tuono_project = TempTuonoProject::new(false);
|
||||
|
||||
temp_tuono_project.add_route("./src/routes/[...all_routes].rs");
|
||||
|
||||
temp_tuono_project.add_api(
|
||||
"./src/routes/api/[...all_apis].rs",
|
||||
&format!("{POST_API_FILE}"),
|
||||
);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/[...all_apis].rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_dyn__catch_all_all_apis;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/[...all_routes].rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod dyn__catch_all_all_routes;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/*all_apis", post(api_dyn__catch_all_all_apis::post__tuono_internal_api))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/*all_routes", get(dyn__catch_all_all_routes::route))"#));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/__tuono/data/*all_routes", get(dyn__catch_all_all_routes::api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_an_api_only_project() {
|
||||
let temp_tuono_project = TempTuonoProject::new(true);
|
||||
|
||||
temp_tuono_project.add_api(
|
||||
"./src/routes/blog/[slug].rs",
|
||||
&format!("{POST_API_FILE}\n{GET_API_FILE}"),
|
||||
);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/blog/[slug].rs"]"#));
|
||||
|
||||
dbg!(&temp_main_rs_content);
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/blog/:slug", post(api_dyn__catch_all_all_apis::post__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TempTuonoProject {
|
||||
original_dir: PathBuf,
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TempTuonoProject {
|
||||
pub fn new(is_api_only: bool) -> Self {
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
let mut file = File::create(temp_dir.path().join("Cargo.toml"))
|
||||
.expect("Failed to create Cargo.toml file");
|
||||
if is_api_only {
|
||||
file.write_all(
|
||||
b"[dependencies]\ntuono_lib = { version = \"0.1.0\", features = [\"api\"] }",
|
||||
)
|
||||
.expect("Failed to write into Cargo.toml file");
|
||||
} else {
|
||||
file.write_all(b"[dependencies]\ntuono_lib = \"0.1.0\"")
|
||||
.expect("Failed to write into Cargo.toml file");
|
||||
}
|
||||
|
||||
TempTuonoProject {
|
||||
original_dir,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.temp_dir.path()
|
||||
}
|
||||
|
||||
pub fn add_route<'a>(&self, path: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("Route path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create parent route directory");
|
||||
File::create(path).expect("Failed to create the route file");
|
||||
}
|
||||
|
||||
pub fn add_api<'a>(&self, path: &'a str, content: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("Route path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create parent route directory");
|
||||
|
||||
let mut file = File::create(path).expect("Failed to create the route file");
|
||||
file.write_all(content.as_bytes())
|
||||
.expect("Failed to write into API file");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempTuonoProject {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(self.original_dir.to_owned())
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.14.3"
|
||||
version = "0.16.2"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -15,28 +15,43 @@ include = [
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[features]
|
||||
full = ["api", "ssr"]
|
||||
api = []
|
||||
ssr = [
|
||||
"dep:ssr_rs",
|
||||
"dep:erased-serde",
|
||||
"dep:serde_json",
|
||||
"dep:reqwest",
|
||||
"dep:tokio-tungstenite",
|
||||
"dep:tungstenite",
|
||||
"dep:futures-util"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
ssr_rs = "0.7.0"
|
||||
axum = {version = "0.7.5", features = ["json", "ws"]}
|
||||
axum-extra = {version = "0.9.6", features = ["cookie"]}
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
erased-serde = "0.4.5"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7.1"
|
||||
reqwest = {version = "0.12.4", features = ["json", "stream"]}
|
||||
once_cell = "1.19.0"
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.16.2"}
|
||||
regex = "1.10.5"
|
||||
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.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"] }
|
||||
tungstenite = "0.24.0"
|
||||
http = "1.1.0"
|
||||
pin-project = "1.1.7"
|
||||
tower = "0.5.1"
|
||||
colored = "2.1.0"
|
||||
# Check if serde is mandatory for API only project
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
# Check if once_cell is mandatory for API only project
|
||||
once_cell = "1.19.0"
|
||||
|
||||
ssr_rs = { version = "0.7.0", optional = true}
|
||||
erased-serde = {version = "0.4.5", optional = true}
|
||||
serde_json = { version = "1.0", optional = true}
|
||||
serde_urlencoded = {version = "0.7.1" }
|
||||
reqwest = {version = "0.12.4", features = ["json", "stream"], optional = true}
|
||||
|
||||
# Match the same version used internally by axum
|
||||
tokio-tungstenite = {version = "0.24.0", optional = true}
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"], optional = true}
|
||||
tungstenite = {version = "0.24.0", optional = true}
|
||||
tower-http = {version = "0.6.0", features = ["fs"]}
|
||||
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
use crate::{ssr::Js, Payload};
|
||||
use axum::extract::{Path, Request};
|
||||
use axum::response::Html;
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr {
|
||||
use crate::ssr::Js;
|
||||
use crate::Payload;
|
||||
use axum::extract::{Path, Request};
|
||||
use axum::response::Html;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub async fn catch_all(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
request: Request,
|
||||
) -> Html<String> {
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
pub async fn catch_all(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
request: Request,
|
||||
) -> Html<String> {
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
|
||||
let req = crate::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
let req = crate::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
|
||||
// TODO: remove unwrap
|
||||
let payload = Payload::new(&req, &"").client_payload().unwrap();
|
||||
// TODO: remove unwrap
|
||||
let payload = Payload::new(&req, &"").client_payload().unwrap();
|
||||
|
||||
let result = Js::render_to_string(Some(&payload));
|
||||
let result = Js::render_to_string(Some(&payload));
|
||||
|
||||
match result {
|
||||
Ok(html) => Html(html),
|
||||
_ => Html("500 internal server error".to_string()),
|
||||
match result {
|
||||
Ok(html) => Html(html),
|
||||
_ => Html("500 internal server error".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
mod catch_all;
|
||||
mod logger;
|
||||
mod manifest;
|
||||
|
||||
mod mode;
|
||||
mod payload;
|
||||
mod request;
|
||||
mod response;
|
||||
mod server;
|
||||
mod ssr;
|
||||
mod vite_reverse_proxy;
|
||||
mod vite_websocket_proxy;
|
||||
|
||||
pub use mode::Mode;
|
||||
pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::{Props, Response};
|
||||
pub use server::Server;
|
||||
pub use tuono_lib_macros::{api, handler};
|
||||
|
||||
// Re-exports
|
||||
pub use axum;
|
||||
pub use axum_extra::extract::cookie;
|
||||
pub use tokio;
|
||||
|
||||
mod request;
|
||||
pub use request::Request;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
#[cfg(feature = "ssr")]
|
||||
mod manifest;
|
||||
mod response;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod payload;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use payload::Payload;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub use response::response::{Props, Response};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod ssr;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod vite_reverse_proxy;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod vite_websocket_proxy;
|
||||
|
||||
@@ -84,7 +84,7 @@ impl<'a> Payload<'a> {
|
||||
.filter(|path| !path.is_empty())
|
||||
.collect::<Vec<&str>>();
|
||||
|
||||
for dyn_route in dynamic_routes.iter() {
|
||||
'_dynamic_routes_loop: for dyn_route in dynamic_routes.iter() {
|
||||
let dyn_route_segments = dyn_route
|
||||
.split('/')
|
||||
.filter(|path| !path.is_empty())
|
||||
@@ -93,6 +93,20 @@ impl<'a> Payload<'a> {
|
||||
let mut route_segments_collector: Vec<&str> = vec![];
|
||||
|
||||
for i in 0..dyn_route_segments.len() {
|
||||
if dyn_route_segments[i].starts_with("[...") {
|
||||
route_segments_collector.push(dyn_route_segments[i]);
|
||||
|
||||
let manifest_key = route_segments_collector.join("/");
|
||||
|
||||
let route_data = manifest.get(&format!("/{manifest_key}"));
|
||||
if let Some(data) = route_data {
|
||||
js_bundles_sources.push(&data.file);
|
||||
data.css
|
||||
.iter()
|
||||
.for_each(|source| css_bundles_sources.push(source))
|
||||
}
|
||||
break '_dynamic_routes_loop;
|
||||
}
|
||||
if path_segments.len() == i {
|
||||
break;
|
||||
}
|
||||
@@ -168,6 +182,14 @@ mod tests {
|
||||
css: vec!["assets/posts/[post]/[comment].css".to_string()],
|
||||
},
|
||||
);
|
||||
manifest_mock.insert(
|
||||
"/pokemons/[...catch_all]".to_string(),
|
||||
BundleInfo {
|
||||
file: "assets/catch_all.js".to_string(),
|
||||
css: vec!["assets/catch_all.css".to_string()],
|
||||
},
|
||||
);
|
||||
|
||||
manifest_mock.insert(
|
||||
"/posts/custom-post".to_string(),
|
||||
BundleInfo {
|
||||
@@ -274,6 +296,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_load_the_correct_catch_all_bundles() {
|
||||
let mut payload = prepare_payload(
|
||||
Some("http://localhost:3000/pokemons/a-poke/a-poke"),
|
||||
Mode::Prod,
|
||||
);
|
||||
let _ = payload.client_payload();
|
||||
assert_eq!(
|
||||
payload.js_bundles,
|
||||
Some(vec![
|
||||
&"assets/bundled-file.js".to_string(),
|
||||
&"assets/catch_all.js".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
payload.css_bundles,
|
||||
Some(vec![
|
||||
&"assets/bundled-file.css".to_string(),
|
||||
&"assets/catch_all.css".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn should_load_the_defined_path_bundles() {
|
||||
let mut payload = prepare_payload(Some("http://localhost:3000/about"), Mode::Prod);
|
||||
|
||||
@@ -1,103 +1,163 @@
|
||||
use crate::Request;
|
||||
use crate::{ssr::Js, Payload};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Redirect};
|
||||
use axum::Json;
|
||||
use erased_serde::Serialize;
|
||||
#[cfg(feature = "ssr")]
|
||||
pub(crate) mod response {
|
||||
use crate::Request;
|
||||
use crate::{ssr::Js, Payload};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Redirect};
|
||||
use axum::Json;
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use erased_serde::Serialize;
|
||||
|
||||
pub struct Props {
|
||||
data: Box<dyn Serialize>,
|
||||
http_code: StatusCode,
|
||||
}
|
||||
|
||||
pub enum Response {
|
||||
Redirect(String),
|
||||
Props(Props),
|
||||
// TODO: improve this tuple to support a more generic IntoResponse
|
||||
Custom((StatusCode, HeaderMap, String)),
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonResponseInfo {
|
||||
redirect_destination: Option<String>,
|
||||
}
|
||||
|
||||
impl JsonResponseInfo {
|
||||
fn new(redirect_destination: Option<String>) -> JsonResponseInfo {
|
||||
JsonResponseInfo {
|
||||
redirect_destination,
|
||||
}
|
||||
pub struct Props {
|
||||
data: Box<dyn Serialize>,
|
||||
http_code: StatusCode,
|
||||
cookies: CookieJar,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonResponse<'a> {
|
||||
data: Option<&'a dyn Serialize>,
|
||||
info: JsonResponseInfo,
|
||||
}
|
||||
pub enum Response {
|
||||
Redirect(String),
|
||||
Props(Props),
|
||||
// TODO: improve this tuple to support a more generic IntoResponse
|
||||
Custom((StatusCode, HeaderMap, String)),
|
||||
}
|
||||
|
||||
impl<'a> JsonResponse<'a> {
|
||||
fn new(props: &'a dyn Serialize) -> Self {
|
||||
JsonResponse {
|
||||
data: Some(props),
|
||||
info: JsonResponseInfo::new(None),
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonResponseInfo {
|
||||
redirect_destination: Option<String>,
|
||||
}
|
||||
|
||||
impl JsonResponseInfo {
|
||||
fn new(redirect_destination: Option<String>) -> JsonResponseInfo {
|
||||
JsonResponseInfo {
|
||||
redirect_destination,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_redirect(destination: String) -> Self {
|
||||
JsonResponse {
|
||||
data: None,
|
||||
info: JsonResponseInfo::new(Some(destination)),
|
||||
}
|
||||
#[derive(serde::Serialize)]
|
||||
struct JsonResponse<'a> {
|
||||
data: Option<&'a dyn Serialize>,
|
||||
info: JsonResponseInfo,
|
||||
}
|
||||
}
|
||||
|
||||
impl Props {
|
||||
pub fn new(data: impl Serialize + 'static) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code: StatusCode::OK,
|
||||
impl<'a> JsonResponse<'a> {
|
||||
fn new(props: &'a dyn Serialize) -> Self {
|
||||
JsonResponse {
|
||||
data: Some(props),
|
||||
info: JsonResponseInfo::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_redirect(destination: String) -> Self {
|
||||
JsonResponse {
|
||||
data: None,
|
||||
info: JsonResponseInfo::new(Some(destination)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code,
|
||||
impl Props {
|
||||
pub fn new(data: impl Serialize + 'static) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code: StatusCode::OK,
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&mut self, http_code: StatusCode) {
|
||||
self.http_code = http_code;
|
||||
}
|
||||
|
||||
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
|
||||
Props {
|
||||
data: Box::new(data),
|
||||
http_code,
|
||||
cookies: CookieJar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cookie(&mut self, cookie: Cookie) {
|
||||
let jar = self.cookies.clone().add(cookie.into_owned());
|
||||
self.cookies = jar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props { data, http_code }) => {
|
||||
let payload = Payload::new(&req, data).client_payload().unwrap();
|
||||
impl Response {
|
||||
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props {
|
||||
data,
|
||||
http_code,
|
||||
cookies,
|
||||
}) => {
|
||||
let payload = Payload::new(&req, data.as_ref()).client_payload().unwrap();
|
||||
|
||||
match Js::render_to_string(Some(&payload)) {
|
||||
Ok(html) => (*http_code, Html(html)),
|
||||
Err(_) => (*http_code, Html("500 Internal server error".to_string())),
|
||||
match Js::render_to_string(Some(&payload)) {
|
||||
Ok(html) => (*http_code, cookies.clone(), Html(html)),
|
||||
Err(_) => (
|
||||
*http_code,
|
||||
cookies.clone(),
|
||||
Html("500 Internal server error".to_string()),
|
||||
),
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
.into_response()
|
||||
Self::Redirect(to) => Redirect::permanent(to).into_response(),
|
||||
Self::Custom(response) => response.clone().into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json(&self) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props {
|
||||
data,
|
||||
http_code,
|
||||
cookies,
|
||||
}) => (
|
||||
*http_code,
|
||||
cookies.clone(),
|
||||
Json(JsonResponse::new(data.as_ref())),
|
||||
)
|
||||
.into_response(),
|
||||
Self::Redirect(destination) => (
|
||||
StatusCode::PERMANENT_REDIRECT,
|
||||
Json(JsonResponse::new_redirect(destination.to_string())),
|
||||
)
|
||||
.into_response(),
|
||||
// Custom never needs the "data" response since its scope
|
||||
// is outside the react domain
|
||||
Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(),
|
||||
}
|
||||
Self::Redirect(to) => Redirect::permanent(to).into_response(),
|
||||
Self::Custom(response) => response.clone().into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json(&self) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props { data, http_code }) => {
|
||||
(*http_code, Json(JsonResponse::new(data))).into_response()
|
||||
}
|
||||
Self::Redirect(destination) => (
|
||||
StatusCode::PERMANENT_REDIRECT,
|
||||
Json(JsonResponse::new_redirect(destination.to_string())),
|
||||
)
|
||||
.into_response(),
|
||||
// Custom never needs the "data" response since its scope
|
||||
// is outside the react domain
|
||||
Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(),
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_update_the_props_status_and_cookie() {
|
||||
let mut props = Props::new("{}");
|
||||
props.status(StatusCode::NOT_FOUND);
|
||||
props.add_cookie(Cookie::new("test", "cookie"));
|
||||
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
props.cookies.get("test").unwrap(),
|
||||
&Cookie::new("test", "cookie")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_add_a_cookie_jar() {
|
||||
let mut props = Props::new("{}");
|
||||
props.status(StatusCode::NOT_FOUND);
|
||||
props.add_cookie(Cookie::new("test", "cookie"));
|
||||
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
props.cookies.get("test").unwrap(),
|
||||
&Cookie::new("test", "cookie")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
use crate::manifest::load_manifest;
|
||||
use crate::mode::{Mode, GLOBAL_MODE};
|
||||
use axum::routing::{get, Router};
|
||||
use axum::routing::Router;
|
||||
use colored::Colorize;
|
||||
use ssr_rs::Ssr;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::logger::LoggerLayer;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::{
|
||||
catch_all::catch_all, logger::LoggerLayer, vite_reverse_proxy::vite_reverse_proxy,
|
||||
catch_all::catch_all, vite_reverse_proxy::vite_reverse_proxy,
|
||||
vite_websocket_proxy::vite_websocket_proxy,
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::manifest::load_manifest;
|
||||
#[cfg(feature = "ssr")]
|
||||
use axum::routing::get;
|
||||
#[cfg(feature = "ssr")]
|
||||
use ssr_rs::Ssr;
|
||||
#[cfg(feature = "ssr")]
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
const DEV_PUBLIC_DIR: &str = "public";
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
const PROD_PUBLIC_DIR: &str = "out/client";
|
||||
|
||||
pub struct Server {
|
||||
@@ -19,6 +31,7 @@ pub struct Server {
|
||||
}
|
||||
|
||||
impl Server {
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn init(router: Router, mode: Mode) -> Server {
|
||||
Ssr::create_platform();
|
||||
|
||||
@@ -31,6 +44,14 @@ impl Server {
|
||||
Server { router, mode }
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn init(router: Router, mode: Mode) -> Server {
|
||||
GLOBAL_MODE.set(mode).unwrap();
|
||||
|
||||
Server { router, mode }
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub async fn start(&self) {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
|
||||
@@ -69,4 +90,24 @@ impl Server {
|
||||
.expect("Failed to serve production server");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub async fn start(&self) {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
|
||||
if self.mode == Mode::Dev {
|
||||
println!(" Ready at: {}\n", "http://localhost:3000".blue().bold());
|
||||
} else {
|
||||
println!(
|
||||
" Production server at: {}\n",
|
||||
"http://localhost:3000".blue().bold()
|
||||
);
|
||||
}
|
||||
|
||||
let router = self.router.to_owned().layer(LoggerLayer::new());
|
||||
|
||||
axum::serve(listener, router)
|
||||
.await
|
||||
.expect("Failed to serve production server");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ struct ProdJs;
|
||||
impl ProdJs {
|
||||
thread_local! {
|
||||
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
||||
Ssr::from(
|
||||
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
|
||||
).unwrap()
|
||||
)
|
||||
Ssr::from(
|
||||
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
|
||||
).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.14.3"
|
||||
version = "0.16.2"
|
||||
edition = "2021"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -15,8 +15,7 @@ include = [
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate_type = ["proc-macro"]
|
||||
proc-marco = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0.0", features = ["full"] }
|
||||
|
||||