Compare commits

...

84 Commits

Author SHA1 Message Date
Valerio Ageno 12a2144a0d feat: add tuono-api example 2024-12-14 12:15:13 +01:00
Valerio Ageno adf7ba5391 feat: add CLI support to api-only mode 2024-12-14 12:15:13 +01:00
Valerio Ageno edb21ab969 feat: add feature flat to tuono_lib 2024-12-14 12:15:12 +01:00
Valerio Ageno 4abc163a59 feat: update version to v0.16.2 2024-12-11 14:04:22 +01:00
Marco Pasqualetti 5bb9cf1e7c fix(packages/tuono): styles are not in the page when running tuono dev (#220) 2024-12-11 13:59:47 +01:00
Valerio Ageno a0cdf408a1 feat: update version to v0.16.1 2024-12-10 19:33:48 +01:00
Valerio Ageno 8467506915 feat: create [...catch_all] route (#210) 2024-12-10 19:30:35 +01:00
Valerio Ageno 704255774d fix: defining-routes.mdx 2024-12-10 08:22:03 +01:00
Valerio Ageno 10bb7b1cca feat: update polyfill-db json shape 2024-12-09 20:29:53 +01:00
Valerio Ageno 7f82af3d3b feat: add polyfill db to documentation (#213)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-09 12:23:33 +01:00
Marco Pasqualetti d7c729c035 docs: update tuono to 0.16 and add mdx support (#214) 2024-12-09 08:20:18 +01:00
Marco Pasqualetti 51fa904f9b docs: optimize public images (#215) 2024-12-09 07:57:25 +01:00
Valerio Ageno 6191e55745 Revert "Add ️ as default favicon for documentation and examples (#191) (#192)"
This reverts commit 7f9bf08f76.
2024-12-08 10:48:15 +01:00
Marco Pasqualetti c6e93c266a feat(packages/tuono): config – add optimizeDeps and plugins to vite options (#197) 2024-12-08 10:44:09 +01:00
Valerio Ageno bcf7e5b5ab docs: add link and navigation page content (#211)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-08 10:18:20 +01:00
Valerio Ageno 466f122cc4 feat: update version to v0.16.0 2024-12-08 10:13:45 +01:00
Valerio Ageno 539484ea25 docs: fill defining-routes page (#207)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2024-12-07 14:51:57 +01:00
Valerio Ageno db1cee25bf test: add integration tests for api/ build (#208) 2024-12-07 14:17:59 +01:00
Marco Pasqualetti 96502b18d0 refactor(apps/documentation): remove lint errors (#206) 2024-12-07 12:28:57 +01:00
Valerio Ageno c491ece8c5 refactor: revamp documentation (#196)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-12-07 12:06:08 +01:00
Marco Pasqualetti c76464bf8a refactor(typescript): use generic instead of simple array type (#205) 2024-12-07 11:58:58 +01:00
Marco Pasqualetti 08b75b1a79 chore(eslint): remove build folder exclusion (#202) 2024-12-07 11:42:38 +01:00
Valerio Ageno 90da869277 feat: update version to v0.15.1 2024-12-07 11:14:13 +01:00
Valerio Ageno 80c4affaa8 feat: add cookies to the response (#201) 2024-12-07 11:10:47 +01:00
Marco Pasqualetti aa087193fe fix(crates/tuono_lib_macros): remove Cargo.tml warnings (#200) 2024-12-06 19:00:54 +01:00
Marco Pasqualetti af7e51f69b chore(ci): setup rust consistently across all workflows (#198) 2024-12-06 14:05:03 +01:00
Marco Pasqualetti 1b62d8ddc2 refactor(crates/tuono): build - ssg - disable zombie_processes warning and use TUONO_PORT to generate server URL (#199) 2024-12-06 13:05:24 +01:00
Valerio Ageno 0839579733 fix: documentation burger menu 2024-12-03 19:38:58 +01:00
Nayan Radadiya 7f9bf08f76 Add ️ as default favicon for documentation and examples (#191) (#192) 2024-12-03 19:35:25 +01:00
Valerio Ageno 79ec1c6922 feat: remove home page from documentation website (#187)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-12-03 16:29:30 +01:00
Valerio Ageno c68246df83 chore: update contributing documentation (#194) 2024-12-03 16:08:49 +01:00
Marco Pasqualetti abc0ad7120 chore(github actions): refine on paths (#189) 2024-12-03 15:59:10 +01:00
Valerio Ageno 4356ca1cff chore: update CONTRIBUTING.md 2024-12-03 14:03:41 +01:00
Valerio Ageno d6215d009c Update health_check.rs 2024-12-03 09:51:42 +01:00
Marco Pasqualetti 0b501e95a5 chore(apps/documentation): migrate to eslint 9 (#188) 2024-12-01 16:40:58 +01:00
Marco Pasqualetti f157ff37e0 refactor(packages/router): removed unused code (#184) 2024-12-01 11:03:12 +01:00
Marco Pasqualetti 11d425faa9 chore(deps-dev): update eslint to v9 (#147) 2024-12-01 10:07:35 +01:00
Valerio Ageno 5fc68a1926 feat: update documentation to v0.15.0 and fix aliases (#182)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 21:37:02 +01:00
Valerio Ageno ec4577c187 feat: add tuono.config.ts support (#153)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-11-30 19:58:04 +01:00
Valerio Ageno 8052de8ad5 feat: update version to v0.15.0 (#181) 2024-11-30 19:03:01 +01:00
Valerio Ageno 2fda959cd6 feat: update tuono new repository reference (#178)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 14:16:18 +01:00
Valerio Ageno 05fa8a5ae2 feat: update docs and readme repo links (#180) 2024-11-30 14:16:10 +01:00
Valerio Ageno a341b41e9a chore: update repo reference withing manifests (#179)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 14:15:36 +01:00
Valerio Ageno a2e0a4d78a feat: update version to v0.14.6 2024-11-30 14:13:21 +01:00
Valerio Ageno 4fb1d6077c Add tuono cli integration test (#177)
Co-authored-by: KingTheSim <KingTheSim@abv.bg>
Co-authored-by: KingTheSim <122887037+KingTheSim@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-29 16:34:56 +01:00
Valerio Ageno 53ab927544 feat: update version to v0.14.5 2024-11-27 21:07:50 +01:00
Valerio Ageno 897cc2daf2 feat: add --no-js-emit flag to tuono build command (#175)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-27 21:05:45 +01:00
Marco Pasqualetti 73ff31daf5 ci(rust): avoid beta and nightly toolchains to be executed on macos and windows (#174) 2024-11-27 20:40:00 +01:00
Marco Pasqualetti c387a9e17e chore(renovate): enable schedule (#172) 2024-11-27 15:33:14 +01:00
Valerio Ageno 1f92931076 Fix optional application state parameters (#173)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-27 15:25:05 +01:00
renovate[bot] d3105a1532 fix(deps): update dependency turbo to v2.3.3 (#169) 2024-11-27 08:26:54 +01:00
Marco Pasqualetti 7f50f31837 ci: trigger workflow only on push only on main (#166) 2024-11-26 21:49:14 +01:00
renovate[bot] 4c3e90ea4b chore(deps): update dependency @vitejs/plugin-react to v4.3.4 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:57 +01:00
renovate[bot] 8d80fc4aa9 chore(deps): update dependency vitest to v2.1.6 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:38 +01:00
renovate[bot] 78a04ffcf1 chore(deps): update dependency prettier to v3.4.1 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:23 +01:00
Valerio Ageno bcb2c91823 fix: windows path resolution (#167) 2024-11-26 21:37:00 +01:00
Marco Pasqualetti 9058f209dd ci: add Windows and MacOS to test matrix for both rust and typescript (#158) 2024-11-26 17:50:53 +01:00
renovate[bot] 1f172d583e fix(deps): update dependency @vitejs/plugin-react-swc to v3.7.2 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 13:48:13 +01:00
Valerio Ageno 439b4757df Turn main.rs into app.rs (#159)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-26 09:20:10 +01:00
renovate[bot] 9f8809bd51 fix(deps): update dependency @types/node to v22.10.0 (#157) 2024-11-26 08:02:27 +01:00
renovate[bot] 4e326650cc fix(deps): update dependency @types/node to v22.10.0 (#154) 2024-11-26 07:59:15 +01:00
renovate[bot] e27be27692 fix(deps): update dependency turbo to v2.3.2 (#155) 2024-11-26 07:58:58 +01:00
renovate[bot] 88eb2a1897 chore(deps): update dependency prettier to v3.4.0 (#156) 2024-11-26 07:58:43 +01:00
Valerio Ageno 8c84ef73c8 feat: update version to v0.14.1 2024-11-24 18:33:45 +01:00
renovate[bot] 859e49f16c fix(deps): update mantine monorepo to v7.14.2 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 18:16:13 +01:00
renovate[bot] 14ba8b0a23 chore(deps): update dependency typescript to v5.7.2 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-23 21:28:03 +01:00
Marco Pasqualetti d7c70f75f3 docs(CONTRIBUTING): add “Local development” (#136) 2024-11-23 21:04:01 +01:00
Valerio Ageno 2760e3ea08 feat(ci): update spellchecker workflow (#143) 2024-11-23 17:30:05 +01:00
Marco Pasqualetti f2f041fe55 ci: test spellcheck via reviewdog (#137)
* ci: use GITHUB_TOKEN in spell checking workflow

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

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

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

* feat: create basic API handler proc macro

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

* chore: remove unused cargo dependencies

* refactor: tuono base health_check

* fix: remove failing test

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

* feat: expand port availability checking

* refactor: improve port extraction code modularity

* fix: formatting

* fix: clippy

* fix: move port checking logic to tuono crate

* fix(tuono): make main async

* fix: tokio panicking when testing CLI

* feat: working port conflict detection in crate tuono

* fix: passing format and lint checks

* refactor: refactor cli by making it non-async

* refactor: move port constants to module scope and remove unnecessary Result return type from port checker
2024-11-21 09:45:08 +01:00
Valerio Ageno eaa0ba28d8 Update README.md 2024-11-20 18:57:43 +01:00
renovate[bot] 3f3e1120b2 chore(deps): update pnpm to v9.14.2 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:52:19 +01:00
Marco Pasqualetti 89d7a2c740 chore: add examples folder to workspaces and make examples packages name unique (#125) 2024-11-20 13:46:31 +01:00
Valerio Ageno 56c31f0204 feat: update version to v0.13.3 2024-11-20 13:45:37 +01:00
renovate[bot] 338cfb5be2 fix(deps): update dependency @types/node to v22.9.1 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:32 +01:00
renovate[bot] 43f6fe664b chore(deps): update pnpm to v9.14.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:03 +01:00
227 changed files with 4294 additions and 1892 deletions
-106
View File
@@ -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",
},
}
+14 -6
View File
@@ -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
+4 -2
View File
@@ -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
+21 -12
View File
@@ -1,11 +1,14 @@
name: Publish on npm and crates
on:
release:
types: [published]
jobs:
publish-on-npm-dry-run:
publish_npm_dry_run:
name: NPM publish (dry run)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -23,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:
+7 -5
View File
@@ -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
@@ -28,4 +30,4 @@ jobs:
uses: ./.github/actions/install-node-dependencies
- name: Test project
run: pnpm repo:root:format
run: pnpm repo:root:format:check
+42 -19
View File
@@ -2,20 +2,58 @@ name: Rust CI
on:
push:
branches:
- 'main'
paths:
- '.github/**'
- 'Cargo.toml'
- 'crates/**'
pull_request:
paths:
- '.github/**'
- 'Cargo.toml'
- 'crates/**'
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Build and test crates on ${{ matrix.os }} with rust ${{ matrix.toolchain }}
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
toolchain:
- '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 }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- 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
@@ -23,23 +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
build_and_test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Build and test rust crates
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- name: Checkout code
uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
+51 -8
View File
@@ -1,23 +1,66 @@
name: Spell Checker
on: [pull_request]
on:
push:
branches:
- 'main'
pull_request:
jobs:
spell_checking:
name: runner / LanguageTools
name: LanguageTool
runs-on: ubuntu-latest
permissions:
checks: write
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
# Keep this value in order to show the error comments
# also on edited files.
fetch-depth: 2
# If the workflow is triggered by a push use check reporter
#
# If the workflow is triggered by a PR coming from the
# same repository, the author has write access so we can
# use pr-review reporter since the GITHUB_TOKEN will have review
# write permissions
#
# Otherwise use annotation reporter that do not require any
# particular permission
#
# Why this?
# https://github.com/Valerioageno/tuono/pull/137#pullrequestreview-2455455272
- name: Set reporter
id: reporter
env:
EVENT_NAME: ${{ github.event_name }}
FROM_MAIN_REPO: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
run: |
echo "EVENT_NAME: $EVENT_NAME FROM_MAIN_REPO: $FROM_MAIN_REPO"
if [ "$EVENT_NAME" == 'push' ]; then
echo "name=github-check" >> $GITHUB_OUTPUT
elif [ "$FROM_MAIN_REPO" == 'true' ]; then
echo "name=github-pr-review" >> $GITHUB_OUTPUT
else
echo "name=github-pr-annotations" >> $GITHUB_OUTPUT
fi
- name: Review dog languagetool
uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
reporter: 'github-pr-review'
reporter: ${{ steps.reporter.outputs.name }}
# Change reporter level if you need.
level: 'error'
level: 'warning'
patterns: '**/*.md **/*.txt **/*.mdx'
#enabled_categories: 'STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END'
# enabled_categories: "STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END"
disabled_categories: ''
disabled_rules: ''
+22 -5
View File
@@ -2,20 +2,38 @@ 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
build_and_test:
name: Check build and test
timeout-minutes: 15
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
@@ -30,8 +48,7 @@ jobs:
- name: Test project
run: pnpm test
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
lint_and_fmt:
name: Check format and lint
runs-on: ubuntu-latest
timeout-minutes: 15
+3
View File
@@ -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/
+10 -6
View File
@@ -1,5 +1,5 @@
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/typescript.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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?**
+4 -3
View File
@@ -7,8 +7,9 @@ members = [
]
exclude = [
"apps/documentation",
"examples/mdx",
"examples/tuono",
"examples/tutorial",
"examples/with-mdx",
"examples/tuono-app",
"examples/tuono-api",
"examples/tuono-tutorial",
"benches/tuono"
]
+8 -10
View File
@@ -3,12 +3,13 @@
# 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">
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/typescript.yml/badge.svg)
**Tuono is the fastest React framework out there**
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
a strong focus on usability and performance.**
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
@@ -47,13 +48,10 @@ 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
As already mentioned above, I strongly suggest you take a look at the
[tutorial](https://tuono.dev/documentation/tutorial).
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
package manager.
@@ -63,9 +61,9 @@ 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, run it inside the project folder `tuono dev`
Then, to run the local development environment, install the JS dependencies (`npm install`) and run inside the project folder `tuono dev`
Finally, when the project is ready to be deployed, just run `tuono build` to create the final React assets
and to set the server project in `production` mode.
-106
View File
@@ -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",
},
}
+1 -1
View File
@@ -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"] }
+17 -16
View File
@@ -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': {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 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"
}
}
]
}
+19 -1
View File
@@ -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,101 +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"
onClick={close}
leftSection="✨"
/>
<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)
+4 -1
View File
@@ -1,2 +1,5 @@
// declaration.d.ts
declare module '*.css'
declare module '*.css' {
const CSSModule: Record<string, string>
export default CSSModule
}
+40 -11
View File
@@ -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' }]} />
@@ -14,11 +14,11 @@ import Breadcrumbs, { Element } from '../../components/breadcrumbs'
The main reason Tuono is fast is that it loads just the features that are needed for the project.
To define them, you need to fill the `ApplicationState` struct in the `./src/main.rs` file, and
To define them, you need to fill the `ApplicationState` struct in the `./src/app.rs` file, and
they will be automatically available in all the handlers you will define across the application.
```rs
// src/main.rs
// src/app.rs
#[derive(Clone)]
pub struct ApplicationState {
@@ -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' }]} />
@@ -0,0 +1,56 @@
import MetaTags from '@/components/meta-tags'
<MetaTags
title="Tuono - Contributing"
canonical="https://tuono.dev/documentation/contributing"
description="The project is massive - if you like it, do consider contributing!"
/>
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
# Contributing
## TL;DR
The project is massive - if you like it, do consider contributing!
## Getting started
The `tuono` project can mostly be split into the following subdomains:
- The CLI
- The Rust backend
- The React frontend
- The documentation website (which is written with tuono 🚀)
To check what the knowledge requirements are for each domain, check the
[requirements](#requirements) section below.
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
It's not strictly required to know both React (& typescript) and Rust (even though it
would be a great nice to have).
Without taking into account specific cases, we can mostly split the domain requirements by:
- The `CLI` needs Rust knowledge (even though a couple of scenarios might also need Typescript)
- The Backend needs just `Rust`
- The Frontend needs just `React` & `Typescript`
- The documentation website needs just `React` & `Typescript` (or even less, since most of the
code is markdown).
import NavigationButtons from '../../../components/navigation-buttons'
<NavigationButtons
next={{
title: 'Local development',
href: '/documentation/contributing/local-development',
}}
/>
@@ -0,0 +1,113 @@
import MetaTags from '@/components/meta-tags'
<MetaTags
title="Tuono - Contributing - Local development"
canonical="https://tuono.dev/documentation/contributing/local-development"
description="Contribute to Tuono. Learn here how to setup the repository for local development"
/>
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
<Breadcrumbs
breadcrumbs={[
{ label: '✨ Contributing', href: '/documentation/contributing' },
{ label: 'Local development' },
]}
/>
# Local development
Thanks for your interest! In this page, you find the instructions to set up `tuono` on your local environment!
## Setup
### Fork and clone repository
After [forking the repo on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo):
```sh
git clone https://github.com/<your-name-here>/tuono
cd tuono
```
### Rust tool chain
Install the Rust programming language tool chain (`rust` and `cargo`).
Follows instructions in the official [docs](https://rustup.rs/)
### Node.js — runtime
Install `Node.js`.
You can follow the instructions from the [Node official site](https://nodejs.org/en/download/package-manager)
> 💡 This project has a `.nvmrc` file to specify the node version used in development.
>
> Consider to use [nvm](https://github.com/nvm-sh/nvm) so you can run
>
> ```sh
> nvm use
> ```
>
> to simply pick up the correct version!
### Node.js — package manager
We use [`pnpm`](https://pnpm.io) as Node.js package manager.
You can see which version of yarn we use by checking the `packageManager` field in the root `package.json`.
### Pre-flight checks
To check that everything is working properly, run:
```sh
turbo run check-all
cargo build
```
## Tuono development
1. Start tuono frontend build using
```sh
turbo run dev
```
2. In another terminal run
```sh
cargo build
```
To automatically rebuild crates on code change, consider using `cargo-watch` crate
```sh
cargo watch -x build -w crates/
```
3. You can now use the binary inside `/target/debug/tuono` in another folder on your local machine
> Consider adding an alias to your shell setup file
>
> ```sh
> alias t="/path-to-repo/target/debug/tuono"
> ```
## Documentation development
1. Change the current working directory to the documentation folder:
```sh
cd apps/documentation
```
2. Run
```sh
tuono dev
```
3. Open the localhost URL.
> On the documentation remember that `tuono` `npm` package is installed from the registry and
> it is not linked to the repository.
@@ -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={[
@@ -70,7 +70,7 @@ Let's fix these in the next section.
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
You can load them in the `ApplicationState` of your app inside the `./src/main.rs` file. This is the file that will be executed just once at the very beginning of your application.
You can load them in the `ApplicationState` of your app inside the `./src/app.rs` file. This is the file that will be executed just once at the very beginning of your application.
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
@@ -101,7 +101,7 @@ serde = { version = "1.0.202", features = ["derive"] }
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
> (similarly as the package.json for Javascript).
Now let's define the `ApplicationState` in the `./src/main.rs` file.
Now let's define the `ApplicationState` in the `./src/app.rs` file.
```rs
// Import here the just added reqwest library
@@ -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' }]} />
@@ -21,7 +21,7 @@ This tutorial is not meant for people who don't know React - in that case, I sug
If you prefer to just read the code rather than write you can download the finished tutorial project with:
```bash
$ tuono new tutorial --template tutorial
$ tuono new tutorial --template tuono-tutorial
```
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me at [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it)
@@ -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)).
-17
View File
@@ -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 />
</>
)
}
+20 -10
View File
@@ -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
View File
@@ -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');
+29 -17
View File
@@ -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" }]
}
+19
View File
@@ -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
+9 -2
View File
@@ -1,12 +1,12 @@
[package]
name = "tuono"
version = "0.13.2"
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,9 +26,16 @@ 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"
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"
+198 -28
View File
@@ -1,5 +1,9 @@
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;
use std::io::prelude::*;
@@ -8,35 +12,96 @@ use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
use toml::{Table, Value};
use crate::route::Route;
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
const IGNORE_FILES: [&str; 1] = ["__root"];
#[cfg(target_os = "windows")]
const ROUTES_FOLDER_PATH: &str = "\\src\\routes";
#[cfg(target_os = "windows")]
const BUILD_JS_SCRIPT: &str = ".\\node_modules\\.bin\\tuono-build-prod.cmd";
#[cfg(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_main_file: bool,
pub has_app_state: bool,
pub is_api_only_mode: bool,
}
fn has_main_file(base_path: PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/main.rs"))?;
fn has_app_state(base_path: &PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/app.rs"))?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
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().unwrap();
let base_path = std::env::current_dir().expect("Failed to read current_dir");
let mut app = App {
route_map: HashMap::new(),
base_path: base_path.clone(),
has_main_file: has_main_file(base_path).unwrap_or(false),
has_app_state: has_app_state(&base_path).unwrap_or(false),
is_api_only_mode: is_api_only_mode(&base_path),
};
app.collect_routes();
@@ -45,18 +110,31 @@ impl App {
}
fn collect_routes(&mut self) {
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
.expect("Failed to read glob pattern")
.for_each(|entry| {
if self.should_collect_route(&entry) {
self.collect_route(entry)
}
})
glob(
self.base_path
.join("src/routes/**/*.*")
.to_str()
.expect("Failed to glob routes folder"),
)
.expect("Failed to read glob pattern")
.for_each(|entry| {
if self.should_collect_route(&entry) {
self.collect_route(entry)
}
})
}
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
let file_extension = entry.as_ref().unwrap().extension().unwrap();
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
let file_extension = entry
.as_ref()
.unwrap()
.extension()
.expect("Failed to read file extension");
let file_name = entry
.as_ref()
.unwrap()
.file_stem()
.expect("Failed to read file name");
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
return false;
@@ -69,19 +147,25 @@ impl App {
}
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
let entry = path_buf.unwrap();
let base_path_str = self.base_path.to_str().unwrap();
let entry = path_buf.expect("Failed to read glob path");
let base_path_str = self
.base_path
.to_str()
.expect("Failed to read as str base_path");
let path = entry
.to_str()
.unwrap()
.replace(&format!("{base_path_str}/src/routes"), "")
.expect("Failed to read entry as str")
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
// Cleanup windows paths
.replace("\\", "/")
.replace(".rs", "")
.replace(".mdx", "")
.replace(".tsx", "");
if entry.extension().unwrap() == "rs" {
if entry.extension().expect("failed to read entry extension") == "rs" {
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
let mut route = Route::new(path);
let mut route = Route::new(path, self.is_api_only_mode);
route.update_axum_info();
route_map.insert(route);
} else {
@@ -91,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);
}
}
@@ -101,7 +185,7 @@ impl App {
}
pub fn build_react_prod(&self) {
Command::new("./node_modules/.bin/tuono-build-prod")
Command::new(BUILD_JS_SCRIPT)
.output()
.expect("Failed to build the react source");
}
@@ -115,6 +199,31 @@ impl App {
.spawn()
.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();
for (_, route) in self.route_map.clone().into_iter() {
if route.axum_info.is_some() {
acc.insert(Method::GET);
}
if !route.is_api() {
continue;
}
for method in route.api_data.unwrap().methods.into_iter() {
acc.insert(method);
}
}
acc
}
}
#[cfg(test)]
@@ -125,8 +234,27 @@ mod tests {
#[test]
fn should_collect_routes() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this\\[post].rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\UPPERCASE.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\sitemap.xml.rs",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
@@ -173,8 +301,25 @@ mod tests {
#[test]
fn should_create_multi_level_axum_paths() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\any-post.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
@@ -199,7 +344,7 @@ mod tests {
assert_eq!(
app.route_map
.get(path)
.unwrap()
.expect("Failed to get route path")
.axum_info
.as_ref()
.unwrap()
@@ -251,8 +396,23 @@ mod tests {
#[test]
fn should_correctly_parse_routes_with_server_handler() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents/tuono\\src\\routes\\about.tsx",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.tsx",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/about.tsx",
@@ -269,9 +429,19 @@ mod tests {
.into_iter()
.for_each(|(path, expected_has_server_handler)| {
if expected_has_server_handler {
assert!(app.route_map.get(path).unwrap().axum_info.is_some())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_some())
} else {
assert!(app.route_map.get(path).unwrap().axum_info.is_none())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_none())
}
})
}
+61 -9
View File
@@ -11,6 +11,9 @@ use crate::scaffold_project;
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
use crate::watch;
const TUONO_PORT: u16 = 3000;
const VITE_PORT: u16 = 3001;
#[derive(Subcommand, Debug)]
enum Actions {
/// Start the development environment
@@ -20,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 {
@@ -39,12 +46,38 @@ 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) {
let rust_listener = std::net::TcpListener::bind(format!("0.0.0.0:{TUONO_PORT}"));
if let Err(_e) = rust_listener {
eprintln!("Error: Failed to bind to port {}", TUONO_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
TUONO_PORT
);
std::process::exit(1);
}
if mode == Mode::Dev {
let vite_listener = std::net::TcpListener::bind(format!("0.0.0.0:{VITE_PORT}"));
if let Err(_e) = vite_listener {
eprintln!("Error: Failed to bind to port {}", VITE_PORT);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
VITE_PORT
);
std::process::exit(1);
}
}
}
pub fn app() -> std::io::Result<()> {
@@ -52,12 +85,23 @@ pub fn app() -> std::io::Result<()> {
match args.action {
Actions::Dev => {
init_tuono_folder(Mode::Dev)?;
watch::watch().unwrap();
check_ports(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(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
@@ -65,9 +109,14 @@ 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 {
check_ports(Mode::Prod);
println!("SSG: generation started");
let static_dir = PathBuf::from("out/static");
@@ -86,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()
@@ -97,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
+71 -16
View File
@@ -1,17 +1,19 @@
use fs_extra::dir::create_all;
use http::Method;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AxumInfo {
// Path for importing the module
pub module_import: String,
@@ -20,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()
@@ -42,7 +44,7 @@ impl AxumInfo {
};
}
if has_dynamic_path(&path) {
if route.is_dynamic {
return AxumInfo {
module_import: module
.as_str()
@@ -50,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(']', ""),
};
}
@@ -65,24 +71,74 @@ impl AxumInfo {
// TODO: to be extended with common scenarios
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
#[derive(Debug, PartialEq, Eq)]
// TODO: Refine this function to catch
// if the methods are commented.
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
regex
.find_iter(&file)
.map(|proc_macro| {
let http_method = proc_macro
.as_str()
// Extract just the element surrounded by the phrantesist.
.replace("tuono_lib::api(", "")
.replace(")]", "");
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
})
.collect::<Vec<Method>>()
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ApiData {
pub methods: Vec<Method>,
}
impl ApiData {
pub fn new(path: &String, is_api_only_mode: bool) -> Option<Self> {
if !path.starts_with("/api/") && !is_api_only_mode {
return None;
}
let base_path = std::env::current_dir().expect("Failed to get the base_path");
let file_path = base_path
.join(format!("src/routes{path}.rs"))
.to_str()
.unwrap()
.to_string();
let methods = read_http_methods_from_file(&file_path);
Some(ApiData { methods })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
path: String,
pub is_dynamic: bool,
pub axum_info: Option<AxumInfo>,
pub api_data: Option<ApiData>,
}
impl Route {
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, is_api_only_mode),
}
}
pub fn is_api(&self) -> bool {
self.api_data.is_some()
}
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self.path.clone()))
self.axum_info = Some(AxumInfo::new(self))
}
pub fn save_ssg_file(&self, reqwest: &Client) {
@@ -107,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();
@@ -117,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)
@@ -177,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");
@@ -201,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))
}
+11 -5
View File
@@ -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 {
@@ -42,7 +42,7 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
let folder = folder_name.unwrap_or(".".to_string());
// In case of missing select the tuono example
let template = template.unwrap_or("tuono".to_string());
let template = template.unwrap_or("tuono-app".to_string());
let client = blocking::Client::builder()
.user_agent("")
@@ -63,11 +63,17 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
.collect::<Vec<&GithubFile>>();
if new_project_files.is_empty() {
println!("Template not found: {template}");
return;
eprintln!("Error: Template '{template}' not found");
println!("Hint: you can view the available templates at https://github.com/tuono-labs/tuono/tree/main/examples");
std::process::exit(1);
}
if folder != "." {
if Path::new(&folder).exists() {
eprintln!("Error: Directory '{folder}' already exists");
println!("Hint: you can scaffold a tuono project within an existing folder with 'cd {folder} && tuono new .'");
std::process::exit(1);
}
create_dir(&folder).unwrap();
}
+31 -26
View File
@@ -82,13 +82,22 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
module_import,
} = axum_info.as_ref().unwrap();
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
));
}
}
}
}
@@ -113,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();
@@ -122,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 {
@@ -139,8 +148,8 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
.replace("/*MODE*/", mode.as_str())
.replace(
"//MAIN_FILE_IMPORT//",
if app.has_main_file {
r#"#[path="../src/main.rs"]
if app.has_app_state {
r#"#[path="../src/app.rs"]
mod tuono_main_state;
"#
} else {
@@ -149,7 +158,7 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
)
.replace(
"//MAIN_FILE_DEFINITION//",
if app.has_main_file {
if app.has_app_state {
"let user_custom_state = tuono_main_state::main();"
} else {
""
@@ -157,27 +166,23 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
)
.replace(
"//MAIN_FILE_USAGE//",
if app.has_main_file {
if app.has_app_state {
".with_state(user_custom_state)"
} else {
""
},
);
let has_server_handlers = app
.route_map
.iter()
.filter(|(_, route)| route.axum_info.is_some())
.collect::<Vec<(&String, &Route)>>()
.is_empty();
let mut import_http_handler = String::new();
if !has_server_handlers {
return src.replace(
"// AXUM_GET_ROUTE_HANDLER",
"use tuono_lib::axum::routing::get;",
);
let used_http_methods = app.get_used_http_methods();
for method in used_http_methods.into_iter() {
let method = method.to_string().to_lowercase();
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
}
src
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
}
pub fn check_tuono_folder() -> io::Result<()> {
@@ -230,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
+43 -25
View File
@@ -9,10 +9,20 @@ use watchexec_supervisor::job::{start_job, Job};
use crate::mode::Mode;
use crate::source_builder::bundle_axum_source;
#[cfg(target_os = "windows")]
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
#[cfg(target_os = "windows")]
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
#[cfg(not(target_os = "windows"))]
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
#[cfg(not(target_os = "windows"))]
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
fn watch_react_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-watch".into(),
prog: DEV_WATCH_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -34,7 +44,7 @@ fn build_rust_src() -> Job {
fn build_react_ssr_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-ssr".into(),
prog: DEV_SSR_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -42,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;
@@ -68,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
}
}
@@ -76,14 +73,15 @@ pub async fn watch() -> Result<()> {
if should_reload_rust_server {
println!(" Reloading...");
run_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
run_server.start();
rust_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
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
@@ -92,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(())
}
+168
View File
@@ -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))"#
));
}
+73
View File
@@ -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.");
}
}
+33 -18
View File
@@ -1,12 +1,12 @@
[package]
name = "tuono_lib"
version = "0.13.2"
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.13.2"}
# 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"]}
+21 -17
View File
@@ -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()),
}
}
}
+28 -11
View File
@@ -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::handler;
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;
+45 -1
View File
@@ -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);
+139 -79
View File
@@ -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")
);
}
}
}
+46 -5
View File
@@ -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");
}
}
+15 -6
View File
@@ -9,6 +9,16 @@ use std::path::PathBuf;
/// update the SSR result without reloading the whole server.
pub struct Js;
#[cfg(target_os = "windows")]
const PROD_BUNDLE_PATH: &str = ".\\out\\server\\prod-server.js";
#[cfg(target_os = "windows")]
const DEV_BUNDLE_PATH: &str = ".\\.tuono\\server\\dev-server.js";
#[cfg(not(target_os = "windows"))]
const PROD_BUNDLE_PATH: &str = "./out/server/prod-server.js";
#[cfg(not(target_os = "windows"))]
const DEV_BUNDLE_PATH: &str = "./.tuono/server/dev-server.js";
impl Js {
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
@@ -26,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("./out/server/prod-server.js")).expect("Server bundle not found"), ""
).unwrap()
)
Ssr::from(
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
}
}
@@ -38,8 +48,7 @@ struct DevJs;
impl DevJs {
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
Ssr::from(
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
.expect("Server bundle not found"),
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
"",
)
.unwrap()
+3 -4
View File
@@ -1,11 +1,11 @@
[package]
name = "tuono_lib_macros"
version = "0.13.2"
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"] }

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