Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90da869277 | |||
| 80c4affaa8 | |||
| aa087193fe | |||
| af7e51f69b | |||
| 1b62d8ddc2 | |||
| 0839579733 | |||
| 7f9bf08f76 | |||
| 79ec1c6922 | |||
| c68246df83 | |||
| abc0ad7120 | |||
| 4356ca1cff | |||
| d6215d009c | |||
| 0b501e95a5 | |||
| f157ff37e0 | |||
| 11d425faa9 | |||
| 5fc68a1926 | |||
| ec4577c187 | |||
| 8052de8ad5 | |||
| 2fda959cd6 | |||
| 05fa8a5ae2 | |||
| a341b41e9a | |||
| a2e0a4d78a | |||
| 4fb1d6077c | |||
| 53ab927544 | |||
| 897cc2daf2 | |||
| 73ff31daf5 | |||
| c387a9e17e | |||
| 1f92931076 | |||
| d3105a1532 | |||
| 7f50f31837 | |||
| 4c3e90ea4b | |||
| 8d80fc4aa9 | |||
| 78a04ffcf1 | |||
| bcb2c91823 | |||
| 9058f209dd | |||
| 1f172d583e | |||
| 439b4757df | |||
| 9f8809bd51 | |||
| 4e326650cc | |||
| e27be27692 | |||
| 88eb2a1897 | |||
| 8c84ef73c8 | |||
| 859e49f16c | |||
| 14ba8b0a23 | |||
| d7c70f75f3 | |||
| 2760e3ea08 | |||
| f2f041fe55 | |||
| 9ec9700ed4 | |||
| c6b047fc02 | |||
| 6727264e39 | |||
| d1a5fe234d | |||
| e84c7cd276 | |||
| 087813fab9 | |||
| 3a4e7398c6 | |||
| 1630fd7e71 | |||
| 4aa99beaff | |||
| eaa0ba28d8 | |||
| 3f3e1120b2 | |||
| 89d7a2c740 | |||
| 56c31f0204 | |||
| 338cfb5be2 | |||
| 43f6fe664b | |||
| 209c9fb7f7 | |||
| a562f67e8d | |||
| ef0fe5a425 | |||
| b50902d23e | |||
| 55490579ec | |||
| 957d08701f | |||
| 61e1aed35d | |||
| 2034f0ef5f | |||
| fcb92c7f0b | |||
| 28ea1ca1df | |||
| ebc1a458a9 | |||
| df92ef5296 | |||
| 253e0a8378 | |||
| a9fbe4eddd | |||
| cfccc638c6 | |||
| 9f1460cc26 | |||
| f0a4541420 | |||
| d0ce482680 | |||
| 40e2734a4e | |||
| 8552796343 | |||
| 6d72ffb876 | |||
| 65faaf0183 | |||
| 7f7c2e8d6f | |||
| 3be491baa7 | |||
| 48b974ae58 | |||
| c394898fcd | |||
| f22249f3e9 | |||
| 120cec52e9 | |||
| 535cc6b3d9 | |||
| 18a060e2bf | |||
| 7b8165cee6 | |||
| 6331eb64e6 | |||
| 3c38b551f0 | |||
| 8e761347f2 | |||
| e2d44e0e6a | |||
| 607da92c71 | |||
| ae0427604e | |||
| 2aa981c581 | |||
| 5d8ec82502 | |||
| fc9bb52c41 | |||
| 3224e4138f | |||
| 7ea28c4fd3 | |||
| acb5567f6a | |||
| cc877c7c11 | |||
| 87bdddf7cf | |||
| d939b64d64 | |||
| 3f64f71947 | |||
| aaae7db24d | |||
| 48c3934b6e |
@@ -1,105 +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-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,23 +0,0 @@
|
||||
## Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## Expected behaviour
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## How to reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Tooling and environment
|
||||
- Tuono version [e.g. 0.4.0]
|
||||
- OS: [e.g. MacOS, Windows]
|
||||
- Browser: [e.g. chrome, safari]
|
||||
- Node version: [e.g. 20.0.0]
|
||||
- Rust version: [e.g. 1.79.0]
|
||||
- Crate version: [e.g. 1.78.0]
|
||||
- Node Package Manger version: [e.g. pnpm: 9.5.0]
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,90 @@
|
||||
name: Bug report
|
||||
description: File a bug report
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behaviour
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: Steps to reproduce the behavior (code, example repository)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: tuono-version
|
||||
attributes:
|
||||
label: Tuono version
|
||||
placeholder: '[e.g. 0.4.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: os-version
|
||||
attributes:
|
||||
label: OS
|
||||
placeholder: '[e.g. MacOS, Windows]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: browser-version
|
||||
attributes:
|
||||
label: Browser
|
||||
placeholder: '[e.g. chrome, safari]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node version
|
||||
placeholder: '[e.g. 20.0.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: rust-version
|
||||
attributes:
|
||||
label: Rust version
|
||||
placeholder: '[e.g. 1.79.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: create-version
|
||||
attributes:
|
||||
label: Crate version
|
||||
placeholder: '[e.g. 1.78.0]'
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: node-package-manager-version
|
||||
attributes:
|
||||
label: Node Package Manger version
|
||||
placeholder: '[e.g. pnpm: 9.5.0] '
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the bug here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature request]: '
|
||||
labels: ['enhancement']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Install NodeJS Dependencies
|
||||
description: This is a composite GitHub Action that sets up pnpm, node and installs the project's dependencies.
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'Explicit node version. Otherwise fallback reading `.nvmrc`. Use in conjunction with matrix'
|
||||
required: false
|
||||
registry-url:
|
||||
description: 'https://github.com/actions/setup-node?tab=readme-ov-file#usage'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js (via input)
|
||||
if: ${{ inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Node.js (via .nvmrc)
|
||||
if: ${{ !inputs.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: ${{ inputs.registry-url }}
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -1,7 +1,7 @@
|
||||
## Context & Description
|
||||
|
||||
<!--
|
||||
Thank you for your Pull Request.
|
||||
Thank you for your Pull Request.
|
||||
|
||||
Explain the context and why you're making that change. What is the problem
|
||||
you're trying to solve? If a new feature is being added, describe the intended
|
||||
|
||||
@@ -2,74 +2,64 @@ 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
|
||||
timeout-minutes: 15
|
||||
name: Check build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/documentation
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.10.4
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-workspace
|
||||
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
|
||||
code_check:
|
||||
name: Check format, lint and types
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/documentation
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-workspace
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
run: pnpm docs:format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
run: pnpm docs:lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
run: pnpm docs:types
|
||||
|
||||
@@ -1,45 +1,36 @@
|
||||
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
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/documentation
|
||||
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.10.4
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-workspace
|
||||
run: cargo install tuono@0.15.0
|
||||
|
||||
- name: Build project
|
||||
working-directory: ./apps/documentation
|
||||
run: tuono build --static
|
||||
|
||||
- name: Deploy
|
||||
|
||||
@@ -1,78 +1,68 @@
|
||||
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
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
dry-run: true
|
||||
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
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Clone root README.md on tuono package
|
||||
run: cp README.md packages/tuono/README.md
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
@@ -81,17 +71,22 @@ 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:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Repo root CI
|
||||
|
||||
# All workflows are triggered based on specific paths,
|
||||
# the root repository is something that is not applicable to rust nor typescript
|
||||
# so it has been moved in a standalone workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- '.github/**'
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Check format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Test project
|
||||
run: pnpm repo:root:format:check
|
||||
@@ -2,39 +2,67 @@ name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'Cargo.toml'
|
||||
- 'crates/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'Cargo.toml'
|
||||
- 'crates/**'
|
||||
|
||||
env:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
lint_and_fmt:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
build_and_test:
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
name: Build and test rust crates
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
- beta
|
||||
- nightly
|
||||
- 'stable'
|
||||
|
||||
# Test `beta` and `nightly` toolchains only on `ubuntu`
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations
|
||||
include:
|
||||
- toolchain: 'beta'
|
||||
os: 'ubuntu-latest'
|
||||
- toolchain: 'nightly'
|
||||
os: 'ubuntu-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||
- 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:
|
||||
name: Check format and lint rust crates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- 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
|
||||
|
||||
@@ -1,21 +1,66 @@
|
||||
name: Spell Checker
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
spell_checking:
|
||||
name: runner / LanguageTools
|
||||
name: LanguageTool
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: reviewdog/action-languagetool@v1
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
github_token: ${{ secrets.REVIEWDOG_TOKEN }}
|
||||
# Keep this value in order to show the error comments
|
||||
# also on edited files.
|
||||
fetch-depth: 2
|
||||
|
||||
# If the workflow is triggered by a push use check reporter
|
||||
#
|
||||
# If the workflow is triggered by a PR coming from the
|
||||
# same repository, the author has write access so we can
|
||||
# use pr-review reporter since the GITHUB_TOKEN will have review
|
||||
# write permissions
|
||||
#
|
||||
# Otherwise use annotation reporter that do not require any
|
||||
# particular permission
|
||||
#
|
||||
# Why this?
|
||||
# https://github.com/Valerioageno/tuono/pull/137#pullrequestreview-2455455272
|
||||
- name: Set reporter
|
||||
id: reporter
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
FROM_MAIN_REPO: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
|
||||
run: |
|
||||
echo "EVENT_NAME: $EVENT_NAME FROM_MAIN_REPO: $FROM_MAIN_REPO"
|
||||
if [ "$EVENT_NAME" == 'push' ]; then
|
||||
echo "name=github-check" >> $GITHUB_OUTPUT
|
||||
elif [ "$FROM_MAIN_REPO" == 'true' ]; then
|
||||
echo "name=github-pr-review" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=github-pr-annotations" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Review dog languagetool
|
||||
uses: reviewdog/action-languagetool@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
|
||||
reporter: 'github-pr-review'
|
||||
reporter: ${{ steps.reporter.outputs.name }}
|
||||
# Change reporter level if you need.
|
||||
level: 'error'
|
||||
level: 'warning'
|
||||
patterns: '**/*.md **/*.txt **/*.mdx'
|
||||
#enabled_categories: 'STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END'
|
||||
# enabled_categories: "STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END"
|
||||
disabled_categories: ''
|
||||
disabled_rules: ''
|
||||
|
||||
@@ -2,65 +2,69 @@ 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
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
- name: Test project
|
||||
run: pnpm test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
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: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Test project
|
||||
run: pnpm test
|
||||
|
||||
lint_and_fmt:
|
||||
name: Check format and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
## Node related ignores
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
.turbo/
|
||||
|
||||
*/**/routeTree.gen.ts
|
||||
|
||||
pnpm-lock.yaml
|
||||
|
||||
examples/playground/
|
||||
examples/*/pnpm-lock.yaml
|
||||
|
||||
## Rust related ignores
|
||||
|
||||
@@ -24,3 +24,6 @@ examples/playground/
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Files, generated by the code editors
|
||||
.vscode/
|
||||
@@ -0,0 +1 @@
|
||||
pnpm-lock.yaml
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
|
||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best, not just for us as individuals, but for the
|
||||
- Focusing on what is best, not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
Advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct that could reasonably be considered inappropriate in a
|
||||
- Other conduct that could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
# How to Contribute to Tuono
|
||||
|
||||
## Contributions
|
||||
## Contributions
|
||||
|
||||
Any feature contribution or suggestion is strongly appreciated.
|
||||
Since the current project size, there isn't yet a defined way to start a discussion. Consider [opening a new issue](https://github.com/Valerioageno/tuono/issues/new)
|
||||
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). 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?**
|
||||
|
||||
- Open a new GitHub pull request with the patch.
|
||||
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number, if applicable.
|
||||
- The pull requests must pass all the CI pipelines
|
||||
|
||||
@@ -7,8 +7,8 @@ members = [
|
||||
]
|
||||
exclude = [
|
||||
"apps/documentation",
|
||||
"examples/mdx",
|
||||
"examples/tuono",
|
||||
"examples/tutorial",
|
||||
"examples/with-mdx",
|
||||
"examples/tuono-app",
|
||||
"examples/tuono-tutorial",
|
||||
"benches/tuono"
|
||||
]
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
|
||||
[✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
|
||||
# Tuono
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||
<img align="right" src="https://raw.githubusercontent.com/tuono-labs/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**Tuono is the fastest React framework out there**
|
||||
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
|
||||
a strong focus on usability and performance.**
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Why Tuono? Just a badass name.
|
||||
|
||||
Some of its features are:
|
||||
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
|
||||
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 NextJs like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
Some of its features are:
|
||||
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
@@ -30,7 +33,7 @@ The [documentation](https://tuono.dev/documentation) is available on
|
||||
|
||||
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)**
|
||||
|
||||
Tuono is a full-stack React framework with the server side written in Rust.
|
||||
Tuono is a full-stack React framework with the server side written in Rust.
|
||||
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.
|
||||
|
||||
@@ -40,12 +43,16 @@ Tuono provides a collection of utilities to handle the server side code seamless
|
||||
Each server-side route is managed in a separate file alongside the React route. The routing is handled
|
||||
by Tuono based on the files defined within the `./src/routes` directory.
|
||||
|
||||
**How is Tuono different from Next.js?**
|
||||
|
||||
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/tuono-labs/tuono/tree/main/benches)).
|
||||
|
||||
## Getting started
|
||||
|
||||
As already mentioned above, I strongly suggest you take a look at the
|
||||
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
||||
|
||||
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
|
||||
Tuono is the CLI that provides all the needed commands to handle the full-stack project.
|
||||
To download it is required [cargo](https://doc.rust-lang.org/cargo/), which is the [rust](https://www.rust-lang.org/)
|
||||
package manager.
|
||||
|
||||
@@ -53,10 +60,10 @@ 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).
|
||||
To create a new project, run `tuono new [NAME]` (optionally, you can pass the `--template` (or `-t`) flag - check the
|
||||
[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.
|
||||
@@ -64,6 +71,7 @@ and to set the server project in `production` mode.
|
||||
Now to execute it just run `cargo run --release`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Any help or suggestion will be appreciated and encouraged.
|
||||
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
|
||||
|
||||
|
||||
@@ -1,105 +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-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
"@typescript-eslint/no-inferrable-types": [
|
||||
"error",
|
||||
{ "ignoreParameters": true },
|
||||
],
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/namespace": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-cycle": "error",
|
||||
"import/no-duplicates": "off",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-unused-modules": "error",
|
||||
"import/order": [
|
||||
"off",
|
||||
{
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index",
|
||||
"object",
|
||||
"type",
|
||||
],
|
||||
},
|
||||
],
|
||||
"no-case-declarations": "error",
|
||||
"no-empty": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-shadow": "error",
|
||||
"no-undef": "off",
|
||||
"sort-imports": "off",
|
||||
},
|
||||
}
|
||||
@@ -8,7 +8,7 @@ name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.11.0"
|
||||
tuono_lib = "0.15.0"
|
||||
glob = "0.3.1"
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"name": "documentation",
|
||||
"description": "The react/rust fullstack framework documentation",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
||||
"types": "tsc --noEmit"
|
||||
@@ -17,24 +19,14 @@
|
||||
"clsx": "^2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "0.11.0"
|
||||
"tuono": "npm:tuono@0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.5.4"
|
||||
"postcss-simple-vars": "^7.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 709 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -1,31 +1,61 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Breadcrumbs, Button } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { Link, Head } from 'tuono'
|
||||
|
||||
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface Breadcrumb {
|
||||
href?: string
|
||||
label: string
|
||||
}
|
||||
interface BreadcrumbsProps {
|
||||
breadcrumbs: Breadcrumb[]
|
||||
}
|
||||
|
||||
export default function TuonoBreadcrumbs({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}): JSX.Element {
|
||||
breadcrumbs = [],
|
||||
}: BreadcrumbsProps): JSX.Element {
|
||||
return (
|
||||
<Breadcrumbs
|
||||
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
|
||||
mb="md"
|
||||
mt="md"
|
||||
>
|
||||
<Button
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
p={5}
|
||||
<>
|
||||
<Head>
|
||||
<script type="application/ld+json">
|
||||
{`{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Tuono - The React/Rust fullstack framework",
|
||||
"item": "https://tuono.dev"
|
||||
}${breadcrumbs.length > 0 ? ',' : ''}
|
||||
${breadcrumbs
|
||||
.map((br, i) =>
|
||||
JSON.stringify({
|
||||
'@type': 'ListItem',
|
||||
position: i + 3,
|
||||
name: br.label,
|
||||
item: br.href ? `https://tuono.dev${br.href}` : undefined,
|
||||
}),
|
||||
)
|
||||
.join(',')}]
|
||||
}
|
||||
`}
|
||||
</script>
|
||||
</Head>
|
||||
<Breadcrumbs
|
||||
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
|
||||
mb="md"
|
||||
mt="md"
|
||||
>
|
||||
<IconBolt />
|
||||
</Button>
|
||||
{children}
|
||||
</Breadcrumbs>
|
||||
<Button href="/" component={Link} variant="subtle" radius="xl" p={5}>
|
||||
<IconBolt />
|
||||
</Button>
|
||||
{breadcrumbs.map((br) => (
|
||||
<BreadcrumbElement href={br.href} label={br.label} key={br.label} />
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Button } from '@mantine/core'
|
||||
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,3 +1,4 @@
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
@@ -15,7 +16,7 @@ export default function Hero(): JSX.Element {
|
||||
return (
|
||||
<Container size={1100} my={100}>
|
||||
<Center>
|
||||
<Title order={1}>The React/Rust full-stack framework</Title>
|
||||
<Title order={1}>Superfast fullstack react framework</Title>
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Text fz="18px">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Text, type TextProps } from '@mantine/core'
|
||||
|
||||
export default function MdxBold(props: TextProps): JSX.Element {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX, HTMLAttributes } from 'react'
|
||||
import { Code } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxCode(
|
||||
props: HTMLAttributes<HTMLPreElement>,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AnchorHTMLAttributes } from 'react'
|
||||
import type { JSX, AnchorHTMLAttributes } from 'react'
|
||||
import { Button } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { IconExternalLink } from '@tabler/icons-react'
|
||||
|
||||
@@ -1,9 +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,7 +1,7 @@
|
||||
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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JSX, HTMLAttributes } from 'react'
|
||||
import { Blockquote, Space } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxQuote(
|
||||
props: HTMLAttributes<HTMLQuoteElement>,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Title, type TitleProps } from '@mantine/core'
|
||||
|
||||
export default function MdxTitle(props: TitleProps): JSX.Element {
|
||||
@@ -7,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(' ', '-')}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import MetaTags from './meta-tags'
|
||||
|
||||
export default MetaTags
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Head } from 'tuono'
|
||||
|
||||
interface MetaTagsProps {
|
||||
title: string
|
||||
description?: string
|
||||
type?: 'website' | 'article' | 'book' | 'video.movie' | 'music.song'
|
||||
canonical: string
|
||||
}
|
||||
|
||||
export default function MetaTags({
|
||||
title,
|
||||
description,
|
||||
type = 'article',
|
||||
canonical,
|
||||
}: MetaTagsProps): ReactNode {
|
||||
return (
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
<link rel="canonical" href={canonical} />
|
||||
<meta
|
||||
name="description"
|
||||
content={
|
||||
description ??
|
||||
'The technologies we love seamlessly working together to unleash the highest web performance ever met on React'
|
||||
}
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="ReactJs, Typescript, Rust, JavaScript, Fullstack framework, Web development"
|
||||
/>
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:image" content="https://tuono.dev/og-cover.png" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:site_name" content="Tuono" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@tuono_dev" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="https://tuono.dev/og-cover.png" />
|
||||
</Head>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +1,18 @@
|
||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||
import { IconBrandGithub, IconBook, IconBrandX } from '@tabler/icons-react'
|
||||
import type { JSX } from 'react'
|
||||
import { Flex, ActionIcon, Group } from '@mantine/core'
|
||||
import { IconBrandGithub, IconBrandDiscord } from '@tabler/icons-react'
|
||||
|
||||
import ThemeBtn from '../theme-btn'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function Actions(): JSX.Element {
|
||||
return (
|
||||
<Flex gap={8}>
|
||||
<Group gap={8} visibleFrom="sm">
|
||||
<Button
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
autoContrast
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<Group gap={8}>
|
||||
<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"
|
||||
>
|
||||
@@ -29,12 +21,12 @@ export default function Actions(): JSX.Element {
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="We are also on X!"
|
||||
href="https://x.com/tuono_dev"
|
||||
aria-label="Join the Tuono's community on Discord!"
|
||||
href="https://discord.com/invite/khQzPa654B"
|
||||
target="_blank"
|
||||
component="a"
|
||||
>
|
||||
<IconBrandX />
|
||||
<IconBrandDiscord />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ThemeBtn />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
import Actions from './actions'
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -8,7 +10,6 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
return (
|
||||
<AppShell.Header p="sm">
|
||||
<Flex justify="space-between">
|
||||
@@ -17,14 +18,7 @@ export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
</Button>
|
||||
<Flex align="center" gap={8}>
|
||||
<Actions />
|
||||
{pathname.startsWith('/documentation') && (
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from 'react'
|
||||
import { Box, Button, Text, Title, Flex } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { IconArrowRight, IconArrowLeft } from '@tabler/icons-react'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { JSX, ReactNode } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { NavLink, type NavLinkProps } from '@mantine/core'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { IconChevronRight } from '@tabler/icons-react'
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import type { JSX } from 'react'
|
||||
import { AppShell } from '@mantine/core'
|
||||
|
||||
import SidebarLink from './sidebar-link'
|
||||
|
||||
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||
interface SidebarProps {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export default function Sidebar({ close }: SidebarProps): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<SidebarLink href="/documentation" label="Overview" onClick={close} />
|
||||
<SidebarLink href="/" label="Home" onClick={close} />
|
||||
<SidebarLink
|
||||
href="/documentation/installation"
|
||||
label="Installation"
|
||||
@@ -63,6 +69,12 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||
/>
|
||||
</SidebarLink>
|
||||
<SidebarLink href="/documentation/cli" label="CLI" onClick={close} />
|
||||
<SidebarLink
|
||||
label="Application state"
|
||||
href="/documentation/application-state"
|
||||
onClick={close}
|
||||
/>
|
||||
|
||||
<SidebarLink
|
||||
label="Routing"
|
||||
href="/documentation/routing"
|
||||
@@ -74,12 +86,19 @@ export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
|
||||
<SidebarLink
|
||||
label="Contributing"
|
||||
href="/documentation/contributing"
|
||||
onClick={close}
|
||||
leftSection="✨"
|
||||
/>
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/contributing/local-development"
|
||||
label="Local development"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
</AppShell.Navbar>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/*
|
||||
* Component inspired by: https://github.com/mantinedev/mantine/tree/master/apps/mantine.dev/src/components/TableOfContents
|
||||
*/
|
||||
import type { JSX } from 'react'
|
||||
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'
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JSX } from 'react'
|
||||
import {
|
||||
ActionIcon,
|
||||
useMantineColorScheme,
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
// declaration.d.ts
|
||||
declare module '*.css'
|
||||
declare module '*.css' {
|
||||
const CSSModule: Record<string, string>
|
||||
export default CSSModule
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ReactNode, JSX } from 'react'
|
||||
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
createTheme,
|
||||
MantineProvider,
|
||||
AppShell,
|
||||
Container,
|
||||
} 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
|
||||
}
|
||||
@@ -61,29 +65,14 @@ const theme = createTheme({
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="/favicon-16x16.png"
|
||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡</text></svg>"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
@@ -98,8 +87,13 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||
{children}
|
||||
<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,19 +0,0 @@
|
||||
import type { ReactNode } 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - Application state"
|
||||
canonical="https://tuono.dev/documentation/application-state"
|
||||
description="Learn how to add features to your Tuono application"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Application state' }]} />
|
||||
|
||||
# Application state
|
||||
|
||||
The main reason Tuono is fast is that it loads just the features that are needed for the project.
|
||||
|
||||
To define them, you need to fill the `ApplicationState` struct in the `./src/app.rs` file, and
|
||||
they will be automatically available in all the handlers you will define across the application.
|
||||
|
||||
```rs
|
||||
// src/app.rs
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApplicationState {
|
||||
pub website_name: String,
|
||||
pub base_path: String
|
||||
}
|
||||
|
||||
pub fn main() -> ApplicationState {
|
||||
let website_name = "tuono".to_string();
|
||||
let base_path = "http://localhost:3000".to_string();
|
||||
|
||||
ApplicationState {
|
||||
website_name,
|
||||
base_path
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> For the sake of simplicity here we just enable a `String` but you can add Database connections or
|
||||
> HTTP clients (take a look [here](/documentation/tutorial/api-fetching) for an example with an HTTP client).
|
||||
|
||||
Now the `ApplicationState` is available on all the handlers as following:
|
||||
|
||||
```rs
|
||||
// src/routes/index.rs
|
||||
|
||||
#[tuono_lib::handler]
|
||||
// The first argument always is the Request
|
||||
// The ApplicationState arguments are optional. You can use just the ones you need
|
||||
// to use in the handler (with no specific order).
|
||||
async fn my_handler(req: Request, website_name: String) -> Response {
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - CLI</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/cli" />
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - CLI"
|
||||
canonical="https://tuono.dev/documentation/cli"
|
||||
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>
|
||||
<Element label="CLI" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'CLI' }]} />
|
||||
|
||||
# CLI
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Contributing</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/contributing" />
|
||||
</Head>
|
||||
<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'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="✨ Contributing" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs breadcrumbs={[{ label: '✨ Contributing' }]} />
|
||||
|
||||
# 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.
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Documentation</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation" />
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs />
|
||||
|
||||
# Tuono
|
||||
|
||||
Tuono is a full-stack framework for building React applications using Rust as the
|
||||
backend.
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
|
||||
|
||||
## Introduction
|
||||
|
||||
NodeJs/Deno/Bun are the only runtimes that allow a React app to be full-stack right? (no)
|
||||
|
||||
Tuono is a full-stack React framework with the server side written in Rust.
|
||||
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.
|
||||
|
||||
Rust is a hard language, then writing server side code is hard as well, right? (no again)
|
||||
|
||||
Tuono provides a collection of utilities to handle the server side code seamlessly with
|
||||
the React code. Each server side route is managed in a separate file alongside the React route.
|
||||
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
|
||||
|
||||
## Features
|
||||
|
||||
Some of its features include:
|
||||
|
||||
- 🟦 Full typescript support
|
||||
- 🌐 File system based routing
|
||||
- 🔥 Hot Module Reload
|
||||
- 🍭 SCSS/CSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🧵 Multi-threaded backend
|
||||
- ⌨️ MDX support
|
||||
- ⚙️ Build optimizations
|
||||
- Custom APIs\*
|
||||
- Image optimization\*
|
||||
- Server streamed content\*
|
||||
|
||||
> \*development in progress
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Head } from 'tuono'
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Installation</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/installation" />
|
||||
</Head>
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Installation" />
|
||||
</Breadcrumbs>
|
||||
<MetaTags
|
||||
title="Tuono - Installation"
|
||||
canonical="https://tuono.dev/documentation/installation"
|
||||
description="Tuono is a development environment built in rust and typescript that outputs a website written in both the languages"
|
||||
/>
|
||||
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Installation' }]} />
|
||||
|
||||
# Installation
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Routing</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/routing" />
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing/intro"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Routing" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Routing' }]} />
|
||||
|
||||
# Routing index
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Routing</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/routing/intro" />
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Routing"
|
||||
canonical="https://tuono.dev/documentation/routing"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Routing" href="/documentation/routing" />
|
||||
<Element label="Project structure" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Routing', href: '/documentation/routing' },
|
||||
{ label: 'Project structure' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Project structure
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - API fetching</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/api-fetching"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - API fetching"
|
||||
canonical="https://tuono.dev/documentation/tutorial/api-fetching"
|
||||
description="Learn how to fetch remote APIs on the backend"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="API fetching" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'API fetching' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# API fetching
|
||||
|
||||
@@ -28,7 +28,7 @@ Clear the `index.rs` file and paste:
|
||||
```rust
|
||||
// src/routes/index.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::reqwest::Client;
|
||||
use reqwest::Client;
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
@@ -56,10 +56,80 @@ async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
|
||||
}
|
||||
```
|
||||
|
||||
> The first argument is always the request `req: Request` which contains all the request's data like the query parameters and the HTTP headers.
|
||||
> The rest of the arguments represents the [ApplicationState](/documentation/application-state) and are optional.
|
||||
|
||||
The terminal will complain now for two reasons:
|
||||
|
||||
1. We don't have imported any `reqwest` crate
|
||||
2. The second argument `fetch: Client` has not been defined yet as global state.
|
||||
|
||||
Let's fix these in the next section.
|
||||
|
||||
## Application state
|
||||
|
||||
Compared to the common Javascript runtimes, Tuono is fast because only the features you need for your project will be loaded.
|
||||
|
||||
You can load them in the `ApplicationState` of your app inside the `./src/app.rs` file. This is the file that will be executed just once at the very beginning of your application.
|
||||
|
||||
> For the tutorial we will use [Reqwest](https://docs.rs/reqwest/latest/reqwest/) which is one of the most famous HTTP library.
|
||||
|
||||
To install it just run in your terminal:
|
||||
|
||||
```bash
|
||||
$ cargo add reqwest
|
||||
```
|
||||
|
||||
A new entry has just been added to your `Cargo.toml` file.
|
||||
|
||||
```diff
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.14.0" # the version might be different
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
++ reqwest = "0.12.9" # the version might be different
|
||||
```
|
||||
|
||||
> The `Cargo.toml` is the manifest file of your application, in which you handle Rust's dependencies
|
||||
> (similarly as the package.json for Javascript).
|
||||
|
||||
Now let's define the `ApplicationState` in the `./src/app.rs` file.
|
||||
|
||||
```rs
|
||||
// Import here the just added reqwest library
|
||||
use reqwest::Client;
|
||||
|
||||
#[derive(Clone)]
|
||||
// Extend this struct with the feature you will need for your application
|
||||
pub struct ApplicationState {
|
||||
// This will be available to all your route handlers
|
||||
pub fetch: Client,
|
||||
}
|
||||
|
||||
pub fn main() -> ApplicationState {
|
||||
let fetch = Client::new();
|
||||
return ApplicationState { fetch };
|
||||
}
|
||||
```
|
||||
|
||||
Now the `fetch: Client` argument is available in the above defined handler, and the terminal should not complain anymore.
|
||||
Let's see in the next section how to show the fetched data on the browser.
|
||||
|
||||
## Handling the page UI
|
||||
|
||||
Now the Pokémon are correctly fetched and hydrated on the client side, so we can actually use them. Clear the `index.tsx` file and paste:
|
||||
|
||||
```tsx
|
||||
// src/routes/index.tsx
|
||||
import type { JSX } from 'react'
|
||||
import type { TuonoProps } from 'tuono'
|
||||
|
||||
interface Pokemon {
|
||||
@@ -72,9 +142,9 @@ interface IndexProps {
|
||||
|
||||
export default function IndexPage({
|
||||
data,
|
||||
}: TuonoProps<IndexProps>): JSX.Element {
|
||||
}: TuonoProps<IndexProps>): JSX.Element | null {
|
||||
if (!data?.results) {
|
||||
return <></>
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Components</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/components"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Components"
|
||||
canonical="https://tuono.dev/documentation/tutorial/components"
|
||||
description="Learn how to manage the components in a Tuono codebase"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Components" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Components' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Components
|
||||
|
||||
@@ -25,6 +25,7 @@ Create the following file `src/components/PokemonLink.tsx` and fill the content
|
||||
|
||||
```tsx
|
||||
// src/components/PokemonLink.tsx
|
||||
import type { JSX } from 'react'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
interface Pokemon {
|
||||
@@ -109,8 +110,9 @@ Then import the styles into the `PokemonLink` component as following:
|
||||
|
||||
```diff
|
||||
// src/components/PokemonLink.tsx
|
||||
import { Link } from "tuono";
|
||||
import type { Pokemon } from "./../types/pokemon";
|
||||
import type { JSX } from 'react'
|
||||
import { Link } from "tuono"
|
||||
import type { Pokemon } from "./../types/pokemon"
|
||||
++ import styles from './PokemonLink.module.css'
|
||||
|
||||
export default function PokemonLink({
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Conclusion</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/conclusion"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Conclusion"
|
||||
canonical="https://tuono.dev/documentation/tutorial/conclusion"
|
||||
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>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Conclusion" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Conclusion' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Conclusion
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Development setup</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/development-setup"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Development setup"
|
||||
canonical="https://tuono.dev/documentation/tutorial/development-setup"
|
||||
description="Learn how to setup a new Tuono project"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Development setup" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Development setup' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Development setup
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Dynamic routes</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/dynamic-routes"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Dynamic routes"
|
||||
canonical="https://tuono.dev/documentation/tutorial/dynamic-routes"
|
||||
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>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Dynamic routes" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Dynamic routes' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Dynamic routes
|
||||
|
||||
@@ -21,7 +21,7 @@ import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
The homepage is ready. We have the full list of Pokémon and they are all links. Great!
|
||||
|
||||
Now we want to make those links actually point to a real page. Let’s create the \_\_ route.
|
||||
Now we want to make those links actually point to a real page. Let’s create the dynamic route.
|
||||
|
||||
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
|
||||
|
||||
@@ -32,7 +32,7 @@ Let’s first work on the server side file. Paste into the new `[pokemon].rs` fi
|
||||
```rust
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tuono_lib::reqwest::Client;
|
||||
use reqwest::Client;
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
@@ -63,6 +63,7 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||
Then let’s work on the frontend. Paste into the `[pokemon].tsx` file the following code:
|
||||
|
||||
```tsx
|
||||
import type { JSX } from 'react'
|
||||
import { TuonoProps } from 'tuono'
|
||||
import PokemonView from '../../components/PokemonView'
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Error handling</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/error-handling"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Error handling"
|
||||
canonical="https://tuono.dev/documentation/tutorial/error-handling"
|
||||
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>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Error handling" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Error handling' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Error handling
|
||||
|
||||
@@ -27,8 +27,8 @@ Let's see how it works!
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
-- use tuono_lib::reqwest::Client;
|
||||
++ use tuono_lib::reqwest::{Client, StatusCode};
|
||||
-- use reqwest::Client;
|
||||
++ use reqwest::{Client, StatusCode};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
|
||||
@@ -69,8 +69,8 @@ async fn get_pokemon(req: Request, fetch: Client) -> Response {
|
||||
```diff
|
||||
// src/routes/index.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
-- use tuono_lib::reqwest::Client;
|
||||
++ use tuono_lib::reqwest::{Client, StatusCode};
|
||||
-- use reqwest::Client;
|
||||
++ use reqwest::{Client, StatusCode};
|
||||
use tuono_lib::{Props, Request, Response};
|
||||
|
||||
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Tutorial</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/tutorial" />
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Tutorial"
|
||||
canonical="https://tuono.dev/documentation/tutorial"
|
||||
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>
|
||||
<Element label="Tutorial" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs breadcrumbs={[{ label: 'Tutorial' }]} />
|
||||
|
||||
# Tutorial
|
||||
|
||||
@@ -22,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,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Building for production</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/production"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Building for production"
|
||||
canonical="https://tuono.dev/documentation/tutorial/production"
|
||||
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>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Production build" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Production build' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Production build
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - Redirections</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://tuono.dev/documentation/tutorial/redirections"
|
||||
/>
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - Redirection"
|
||||
canonical="https://tuono.dev/documentation/tutorial/redirections"
|
||||
description="Learn how to redirect to different route from the server"
|
||||
/>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
import Breadcrumbs, { Element } from '@/components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="Redirections" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'Redirections' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# Redirections
|
||||
|
||||
@@ -25,10 +25,10 @@ First, let's create a new route by just creating a new file `/pokemons/GOAT.rs`
|
||||
|
||||
```rs
|
||||
// src/routes/pokemons/GOAT.rs
|
||||
use tuono_lib::{reqwest::Client, Request, Response};
|
||||
use tuono_lib::{Request, Response};
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn redirect_to_goat(_: Request, _: Client) -> Response {
|
||||
async fn redirect_to_goat(_req: Request) -> Response {
|
||||
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
|
||||
Response::Redirect("/pokemons/mewtwo".to_string())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { Head } from 'tuono'
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<Head>
|
||||
<title>Tutorial - SEO and meta tags</title>
|
||||
<link rel="canonical" href="https://tuono.dev/documentation/tutorial/seo" />
|
||||
</Head>
|
||||
<MetaTags
|
||||
title="Tuono - SEO and meta tags"
|
||||
canonical="https://tuono.dev/documentation/tutorial/seo"
|
||||
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>
|
||||
<Element label="Tutorial" href="/documentation/tutorial" />
|
||||
<Element label="SEO and meta tags" />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs
|
||||
breadcrumbs={[
|
||||
{ label: 'Tutorial', href: '/documentation/tutorial' },
|
||||
{ label: 'SEO and meta tags' },
|
||||
]}
|
||||
/>
|
||||
|
||||
# SEO and meta tags
|
||||
|
||||
@@ -23,6 +26,7 @@ To do so `tuono` also exposes the `<Head />` component useful exactly for handli
|
||||
|
||||
```diff
|
||||
// src/routes/index.tsx
|
||||
import type { JSX } from 'react'
|
||||
import type { TuonoProps } from "tuono";
|
||||
++ import { Head } from "tuono"
|
||||
|
||||
@@ -77,6 +81,7 @@ export default function IndexPage({
|
||||
|
||||
```diff
|
||||
// src/routes/pokemons/[pokemon].tsx
|
||||
import type { JSX } from 'react'
|
||||
-- import { TuonoProps } from "tuono";
|
||||
++ import { TuonoProps, Head } from "tuono";
|
||||
import PokemonView from "../../components/PokemonView";
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import MetaTags from '@/components/meta-tags'
|
||||
|
||||
<MetaTags
|
||||
title="Tuono - 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'
|
||||
|
||||
<Breadcrumbs />
|
||||
|
||||
# Tuono
|
||||
|
||||
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.
|
||||
|
||||
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
|
||||
|
||||
## Features
|
||||
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
## Introduction
|
||||
|
||||
Tuono addresses the following two limits:
|
||||
|
||||
- Enable React server-side rendering projects without using JS runtimes (Node, Deno)
|
||||
- Ease the web development using a typically hard language like Rust.
|
||||
|
||||
#### 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/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.
|
||||
|
||||
#### Rust is a hard language, then writing server side code is hard as well, right? (no again)
|
||||
|
||||
Tuono provides a collection of utilities to handle server-side code seamlessly with
|
||||
the React code. Each server side route is managed in a separate file alongside the React route.
|
||||
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
|
||||
|
||||
#### How is Tuono different from Next.js?
|
||||
|
||||
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/tuono-labs/tuono/tree/main/benches)).
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
import Hero from '../components/hero'
|
||||
|
||||
export default function IndexPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Tuono - The React/Rust full-stack framework</title>
|
||||
<link rel="canonical" href="https://tuono.dev" />
|
||||
<meta
|
||||
name="description"
|
||||
content="The technologies we love seamlessly working together to unleash the highest web performance ever met on React"
|
||||
/>
|
||||
</Head>
|
||||
<Hero />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use tuono_lib::reqwest::{Client, 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)
|
||||
}
|
||||
}
|
||||
@@ -36,7 +41,7 @@ fn clean_path(value: String) -> String {
|
||||
}
|
||||
|
||||
#[tuono_lib::handler]
|
||||
async fn generate_sitemap(_req: Request, _fetch: Client) -> Response {
|
||||
async fn generate_sitemap(_req: Request) -> Response {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::CONTENT_TYPE, "text/xml".parse().unwrap());
|
||||
|
||||
@@ -51,11 +56,16 @@ async fn generate_sitemap(_req: Request, _fetch: Client) -> 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,25 +1,37 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
// Typechecking
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Modules
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
// Emit
|
||||
"noEmit": true,
|
||||
|
||||
// JavaScript Support
|
||||
"allowJs": true,
|
||||
|
||||
// Interop Constraints
|
||||
"isolatedModules": true,
|
||||
|
||||
// Language and Environment
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Completeness
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src", "tuono.config.ts", "postcss.config.js"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { TuonoConfig } from 'tuono/config'
|
||||
|
||||
const config: TuonoConfig = {
|
||||
vite: {
|
||||
alias: {
|
||||
'@': 'src',
|
||||
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.11.1"
|
||||
version = "0.15.1"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -20,9 +21,9 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive", "cargo"] }
|
||||
watchexec = "4.0.0"
|
||||
watchexec = "5.0.0"
|
||||
miette = "7.2.0"
|
||||
watchexec-signals = "3.0.0"
|
||||
watchexec-signals = "4.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
watchexec-supervisor = "3.0.0"
|
||||
@@ -31,3 +32,9 @@ 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"
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use glob::glob;
|
||||
use glob::GlobError;
|
||||
use http::Method;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
@@ -11,19 +16,45 @@ 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_app_state: bool,
|
||||
}
|
||||
|
||||
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||
let file = File::open(base_path.join("src/app.rs"))?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
buf_reader.read_to_string(&mut contents)?;
|
||||
Ok(contents.contains("pub fn main"))
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let base_path = std::env::current_dir().unwrap();
|
||||
let base_path = std::env::current_dir().expect("Failed to read current_dir");
|
||||
|
||||
let mut app = App {
|
||||
route_map: HashMap::new(),
|
||||
base_path,
|
||||
base_path: base_path.clone(),
|
||||
has_app_state: has_app_state(base_path).unwrap_or(false),
|
||||
};
|
||||
|
||||
app.collect_routes();
|
||||
@@ -32,18 +63,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;
|
||||
@@ -56,17 +100,23 @@ impl App {
|
||||
}
|
||||
|
||||
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
||||
let entry = path_buf.unwrap();
|
||||
let base_path_str = self.base_path.to_str().unwrap();
|
||||
let entry = path_buf.expect("Failed to read glob path");
|
||||
|
||||
let base_path_str = self
|
||||
.base_path
|
||||
.to_str()
|
||||
.expect("Failed to read as str base_path");
|
||||
let path = entry
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace(&format!("{base_path_str}/src/routes"), "")
|
||||
.expect("Failed to read entry as str")
|
||||
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
|
||||
// Cleanup windows paths
|
||||
.replace("\\", "/")
|
||||
.replace(".rs", "")
|
||||
.replace(".mdx", "")
|
||||
.replace(".tsx", "");
|
||||
|
||||
if entry.extension().unwrap() == "rs" {
|
||||
if entry.extension().expect("failed to read entry extension") == "rs" {
|
||||
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||
let mut route = Route::new(path);
|
||||
route.update_axum_info();
|
||||
@@ -88,7 +138,7 @@ impl App {
|
||||
}
|
||||
|
||||
pub fn build_react_prod(&self) {
|
||||
Command::new("./node_modules/.bin/tuono-build-prod")
|
||||
Command::new(BUILD_JS_SCRIPT)
|
||||
.output()
|
||||
.expect("Failed to build the react source");
|
||||
}
|
||||
@@ -102,6 +152,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)]
|
||||
@@ -112,13 +187,34 @@ mod tests {
|
||||
#[test]
|
||||
fn should_collect_routes() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this\\[post].rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\UPPERCASE.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\sitemap.xml.rs",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/handle-this.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/handle-this/[post].rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
|
||||
"/home/user/Documents/tuono/src/routes/sitemap.xml.rs",
|
||||
];
|
||||
@@ -132,6 +228,11 @@ mod tests {
|
||||
("/about", "about"),
|
||||
("/posts/index", "posts_index"),
|
||||
("/posts/[post]", "posts_dyn_post"),
|
||||
("/posts/handle-this", "posts_handle_hyphen_this"),
|
||||
(
|
||||
"/posts/handle-this/[post]",
|
||||
"posts_handle_hyphen_this_dyn_post",
|
||||
),
|
||||
("/posts/UPPERCASE", "posts_uppercase"),
|
||||
("/sitemap.xml", "sitemap_dot_xml"),
|
||||
];
|
||||
@@ -153,8 +254,25 @@ mod tests {
|
||||
#[test]
|
||||
fn should_create_multi_level_axum_paths() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\any-post.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||
@@ -179,7 +297,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
app.route_map
|
||||
.get(path)
|
||||
.unwrap()
|
||||
.expect("Failed to get route path")
|
||||
.axum_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -231,8 +349,23 @@ mod tests {
|
||||
#[test]
|
||||
fn should_correctly_parse_routes_with_server_handler() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents/tuono\\src\\routes\\about.tsx",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.tsx",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/about.tsx",
|
||||
@@ -249,9 +382,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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,21 @@ pub fn app() -> std::io::Result<()> {
|
||||
|
||||
match args.action {
|
||||
Actions::Dev => {
|
||||
init_tuono_folder(Mode::Dev)?;
|
||||
check_ports(Mode::Dev);
|
||||
|
||||
let app = init_tuono_folder(Mode::Dev)?;
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
|
||||
watch::watch().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 {
|
||||
println!("Rust build successfully finished");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if ssg && app.has_dynamic_routes() {
|
||||
// TODO: allow dynamic routes static generation
|
||||
@@ -65,9 +107,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 +133,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 +146,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
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use http::Method;
|
||||
use regex::Regex;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn has_dynamic_path(route: &str) -> bool {
|
||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||
regex.is_match(route)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct AxumInfo {
|
||||
// Path for importing the module
|
||||
pub module_import: String,
|
||||
@@ -32,6 +34,7 @@ impl AxumInfo {
|
||||
.to_string()
|
||||
.replace('/', "_")
|
||||
.replace('.', "_dot_")
|
||||
.replace('-', "_hyphen_")
|
||||
.to_lowercase();
|
||||
|
||||
if axum_route.is_empty() {
|
||||
@@ -47,6 +50,7 @@ impl AxumInfo {
|
||||
.as_str()
|
||||
.to_string()
|
||||
.replace('/', "_")
|
||||
.replace('-', "_hyphen_")
|
||||
.replace('[', "dyn_")
|
||||
.replace(']', ""),
|
||||
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
||||
@@ -63,11 +67,56 @@ impl AxumInfo {
|
||||
// TODO: to be extended with common scenarios
|
||||
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
// TODO: Refine this function to catch
|
||||
// if the methods are commented.
|
||||
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
|
||||
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
|
||||
|
||||
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
|
||||
|
||||
regex
|
||||
.find_iter(&file)
|
||||
.map(|proc_macro| {
|
||||
let http_method = proc_macro
|
||||
.as_str()
|
||||
// Extract just the element surrounded by the phrantesist.
|
||||
.replace("tuono_lib::api(", "")
|
||||
.replace(")]", "");
|
||||
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
|
||||
})
|
||||
.collect::<Vec<Method>>()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ApiData {
|
||||
pub methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl ApiData {
|
||||
pub fn new(path: &String) -> Option<Self> {
|
||||
if !path.starts_with("/api/") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_path = std::env::current_dir().expect("Failed to get the base_path");
|
||||
|
||||
let file_path = base_path
|
||||
.join(format!("src/routes{path}.rs"))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let methods = read_http_methods_from_file(&file_path);
|
||||
|
||||
Some(ApiData { methods })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Route {
|
||||
path: String,
|
||||
pub is_dynamic: bool,
|
||||
pub axum_info: Option<AxumInfo>,
|
||||
pub api_data: Option<ApiData>,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
@@ -76,9 +125,14 @@ impl Route {
|
||||
path: cleaned_path.clone(),
|
||||
axum_info: None,
|
||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||
api_data: ApiData::new(&cleaned_path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_api(&self) -> bool {
|
||||
self.api_data.is_some()
|
||||
}
|
||||
|
||||
pub fn update_axum_info(&mut self) {
|
||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,17 @@ const MODE: Mode = /*MODE*/;
|
||||
|
||||
// MODULE_IMPORTS
|
||||
|
||||
//MAIN_FILE_IMPORT//
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("\n ⚡ Tuono v/*VERSION*/");
|
||||
|
||||
//MAIN_FILE_DEFINITION//
|
||||
|
||||
let router = Router::new()
|
||||
// ROUTE_BUILDER
|
||||
;
|
||||
//MAIN_FILE_USAGE//;
|
||||
|
||||
Server::init(router, MODE).start().await
|
||||
}
|
||||
@@ -77,13 +82,23 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
module_import,
|
||||
} = axum_info.as_ref().unwrap();
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::route))"#
|
||||
));
|
||||
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||
));
|
||||
if !route.is_api() {
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::route))"#
|
||||
));
|
||||
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||
));
|
||||
} else {
|
||||
for method in route.api_data.as_ref().unwrap().methods.clone() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +123,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();
|
||||
@@ -117,7 +132,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 {
|
||||
@@ -131,22 +146,44 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||
&create_modules_declaration(&app.route_map),
|
||||
)
|
||||
.replace("/*VERSION*/", crate_version!())
|
||||
.replace("/*MODE*/", mode.as_str());
|
||||
|
||||
let has_server_handlers = app
|
||||
.route_map
|
||||
.iter()
|
||||
.filter(|(_, route)| route.axum_info.is_some())
|
||||
.collect::<Vec<(&String, &Route)>>()
|
||||
.is_empty();
|
||||
|
||||
if !has_server_handlers {
|
||||
return src.replace(
|
||||
"// AXUM_GET_ROUTE_HANDLER",
|
||||
"use tuono_lib::axum::routing::get;",
|
||||
.replace("/*MODE*/", mode.as_str())
|
||||
.replace(
|
||||
"//MAIN_FILE_IMPORT//",
|
||||
if app.has_app_state {
|
||||
r#"#[path="../src/app.rs"]
|
||||
mod tuono_main_state;
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"//MAIN_FILE_DEFINITION//",
|
||||
if app.has_app_state {
|
||||
"let user_custom_state = tuono_main_state::main();"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"//MAIN_FILE_USAGE//",
|
||||
if app.has_app_state {
|
||||
".with_state(user_custom_state)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
|
||||
let mut import_http_handler = String::new();
|
||||
|
||||
let used_http_methods = app.get_used_http_methods();
|
||||
|
||||
for method in used_http_methods.into_iter() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
|
||||
}
|
||||
src
|
||||
|
||||
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
||||
}
|
||||
|
||||
pub fn check_tuono_folder() -> io::Result<()> {
|
||||
|
||||
@@ -9,10 +9,20 @@ use watchexec_supervisor::job::{start_job, Job};
|
||||
use crate::mode::Mode;
|
||||
use crate::source_builder::bundle_axum_source;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
|
||||
#[cfg(target_os = "windows")]
|
||||
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
|
||||
|
||||
fn watch_react_src() -> Job {
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-watch".into(),
|
||||
prog: DEV_WATCH_BIN_SRC.into(),
|
||||
args: vec![],
|
||||
},
|
||||
options: Default::default(),
|
||||
@@ -34,7 +44,7 @@ fn build_rust_src() -> Job {
|
||||
fn build_react_ssr_src() -> Job {
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-ssr".into(),
|
||||
prog: DEV_SSR_BIN_SRC.into(),
|
||||
args: vec![],
|
||||
},
|
||||
options: Default::default(),
|
||||
@@ -77,7 +87,7 @@ pub async fn watch() -> Result<()> {
|
||||
if should_reload_rust_server {
|
||||
println!(" Reloading...");
|
||||
run_server.stop();
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
|
||||
run_server.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
mod utils;
|
||||
use assert_cmd::Command;
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
use utils::TempTuonoProject;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_the_index_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
|
||||
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/data.json", get(index::api))"#
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
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() -> 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");
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempTuonoProject {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(self.original_dir.to_owned())
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.11.1"
|
||||
version = "0.15.1"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -14,13 +15,11 @@ include = [
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "tuono_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[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"
|
||||
@@ -33,9 +32,12 @@ 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.11.1"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.15.1"}
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod catch_all;
|
||||
mod logger;
|
||||
mod manifest;
|
||||
mod mode;
|
||||
mod payload;
|
||||
@@ -14,9 +15,9 @@ pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::{Props, Response};
|
||||
pub use server::Server;
|
||||
pub use tuono_lib_macros::handler;
|
||||
pub use tuono_lib_macros::{api, handler};
|
||||
|
||||
// Re-exports
|
||||
pub use axum;
|
||||
pub use reqwest;
|
||||
pub use axum_extra::extract::cookie;
|
||||
pub use tokio;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use colored::Colorize;
|
||||
use http::{method::Method, Request, Response};
|
||||
use pin_project::pin_project;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::time::Instant;
|
||||
use tower::{Layer, Service};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoggerLayer {}
|
||||
|
||||
impl LoggerLayer {
|
||||
pub fn new() -> Self {
|
||||
LoggerLayer {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for LoggerLayer {
|
||||
type Service = Logger<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
Logger::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Logger<S> {
|
||||
inner: S,
|
||||
}
|
||||
|
||||
impl<S> Logger<S> {
|
||||
pub fn new(inner: S) -> Self {
|
||||
Logger { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Logger<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
|
||||
ResBody: Default,
|
||||
<S as Service<Request<ReqBody>>>::Error: Debug,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = LoggerFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
|
||||
LoggerFuture {
|
||||
future: self.inner.call(req),
|
||||
method,
|
||||
path,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pub struct LoggerFuture<F> {
|
||||
#[pin]
|
||||
future: F,
|
||||
method: Method,
|
||||
path: String,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl<F, B, E> Future for LoggerFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response<B>, E>>,
|
||||
B: Default,
|
||||
E: Debug,
|
||||
{
|
||||
type Output = Result<Response<B>, E>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let res: F::Output = match this.future.poll(cx) {
|
||||
Poll::Ready(res) => res,
|
||||
Poll::Pending => return Poll::Pending,
|
||||
};
|
||||
|
||||
if this.path.starts_with("/__tuono/data") {
|
||||
return Poll::Ready(res);
|
||||
}
|
||||
|
||||
let status_code = res.as_ref().unwrap().status();
|
||||
|
||||
println!(
|
||||
" {} {} {} in {}ms",
|
||||
this.method,
|
||||
this.path,
|
||||
status_code.as_str().green(),
|
||||
this.start.elapsed().as_millis()
|
||||
);
|
||||
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ use crate::{ssr::Js, Payload};
|
||||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Redirect};
|
||||
use axum::Json;
|
||||
use colored::*;
|
||||
use axum_extra::extract::cookie::{Cookie, CookieJar};
|
||||
use erased_serde::Serialize;
|
||||
use tokio::time::Instant;
|
||||
|
||||
pub struct Props {
|
||||
data: Box<dyn Serialize>,
|
||||
http_code: StatusCode,
|
||||
cookies: CookieJar,
|
||||
}
|
||||
|
||||
pub enum Response {
|
||||
@@ -59,40 +59,47 @@ impl Props {
|
||||
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 start = Instant::now();
|
||||
let payload = Payload::new(&req, data).client_payload().unwrap();
|
||||
Self::Props(Props {
|
||||
data,
|
||||
http_code,
|
||||
cookies,
|
||||
}) => {
|
||||
let payload = Payload::new(&req, data.as_ref()).client_payload().unwrap();
|
||||
|
||||
let response = 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();
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
println!(
|
||||
" GET {} {} in {}ms",
|
||||
req.uri.path(),
|
||||
http_code.as_str().green(),
|
||||
duration.as_millis()
|
||||
);
|
||||
|
||||
response
|
||||
.into_response()
|
||||
}
|
||||
Self::Redirect(to) => Redirect::permanent(to).into_response(),
|
||||
Self::Custom(response) => response.clone().into_response(),
|
||||
@@ -101,9 +108,16 @@ impl Response {
|
||||
|
||||
pub fn json(&self) -> impl IntoResponse {
|
||||
match self {
|
||||
Self::Props(Props { data, http_code }) => {
|
||||
(*http_code, Json(JsonResponse::new(data))).into_response()
|
||||
}
|
||||
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())),
|
||||
@@ -115,3 +129,32 @@ impl Response {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_update_the_props_status_and_cookie() {
|
||||
let mut props = Props::new("{}");
|
||||
props.status(StatusCode::NOT_FOUND);
|
||||
props.add_cookie(Cookie::new("test", "cookie"));
|
||||
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
props.cookies.get("test").unwrap(),
|
||||
&Cookie::new("test", "cookie")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_add_a_cookie_jar() {
|
||||
let mut props = Props::new("{}");
|
||||
props.status(StatusCode::NOT_FOUND);
|
||||
props.add_cookie(Cookie::new("test", "cookie"));
|
||||
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
|
||||
assert_eq!(
|
||||
props.cookies.get("test").unwrap(),
|
||||
&Cookie::new("test", "cookie")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::mode::{Mode, GLOBAL_MODE};
|
||||
|
||||
use crate::manifest::load_manifest;
|
||||
use crate::mode::{Mode, GLOBAL_MODE};
|
||||
use axum::routing::{get, Router};
|
||||
use colored::Colorize;
|
||||
use ssr_rs::Ssr;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::{
|
||||
catch_all::catch_all, vite_reverse_proxy::vite_reverse_proxy,
|
||||
catch_all::catch_all, logger::LoggerLayer, vite_reverse_proxy::vite_reverse_proxy,
|
||||
vite_websocket_proxy::vite_websocket_proxy,
|
||||
};
|
||||
|
||||
@@ -15,12 +14,12 @@ const DEV_PUBLIC_DIR: &str = "public";
|
||||
const PROD_PUBLIC_DIR: &str = "out/client";
|
||||
|
||||
pub struct Server {
|
||||
router: Router<reqwest::Client>,
|
||||
router: Router,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn init(router: Router<reqwest::Client>, mode: Mode) -> Server {
|
||||
pub fn init(router: Router, mode: Mode) -> Server {
|
||||
Ssr::create_platform();
|
||||
|
||||
GLOBAL_MODE.set(mode).unwrap();
|
||||
@@ -35,17 +34,18 @@ impl Server {
|
||||
pub async fn start(&self) {
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
|
||||
let fetch = reqwest::Client::new();
|
||||
|
||||
if self.mode == Mode::Dev {
|
||||
println!(" Ready at: {}\n", "http://localhost:3000".blue().bold());
|
||||
let router = self
|
||||
.router
|
||||
.to_owned()
|
||||
.layer(LoggerLayer::new())
|
||||
.route("/vite-server/", get(vite_websocket_proxy))
|
||||
.route("/vite-server/*path", get(vite_reverse_proxy))
|
||||
.fallback_service(ServeDir::new(DEV_PUBLIC_DIR).fallback(get(catch_all)))
|
||||
.with_state(fetch);
|
||||
.fallback_service(
|
||||
ServeDir::new(DEV_PUBLIC_DIR)
|
||||
.fallback(get(catch_all).layer(LoggerLayer::new())),
|
||||
);
|
||||
|
||||
axum::serve(listener, router)
|
||||
.await
|
||||
@@ -58,8 +58,11 @@ impl Server {
|
||||
let router = self
|
||||
.router
|
||||
.to_owned()
|
||||
.fallback_service(ServeDir::new(PROD_PUBLIC_DIR).fallback(get(catch_all)))
|
||||
.with_state(fetch);
|
||||
.layer(LoggerLayer::new())
|
||||
.fallback_service(
|
||||
ServeDir::new(PROD_PUBLIC_DIR)
|
||||
.fallback(get(catch_all).layer(LoggerLayer::new())),
|
||||
);
|
||||
|
||||
axum::serve(listener, router)
|
||||
.await
|
||||
|
||||
@@ -9,6 +9,16 @@ use std::path::PathBuf;
|
||||
/// update the SSR result without reloading the whole server.
|
||||
pub struct Js;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const PROD_BUNDLE_PATH: &str = ".\\out\\server\\prod-server.js";
|
||||
#[cfg(target_os = "windows")]
|
||||
const DEV_BUNDLE_PATH: &str = ".\\.tuono\\server\\dev-server.js";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const PROD_BUNDLE_PATH: &str = "./out/server/prod-server.js";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const DEV_BUNDLE_PATH: &str = "./.tuono/server/dev-server.js";
|
||||
|
||||
impl Js {
|
||||
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
|
||||
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
|
||||
@@ -27,7 +37,7 @@ impl ProdJs {
|
||||
thread_local! {
|
||||
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
|
||||
Ssr::from(
|
||||
read_to_string(PathBuf::from("./out/server/prod-server.js")).expect("Server bundle not found"), ""
|
||||
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
|
||||
).unwrap()
|
||||
)
|
||||
}
|
||||
@@ -38,8 +48,7 @@ struct DevJs;
|
||||
impl DevJs {
|
||||
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
|
||||
Ssr::from(
|
||||
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
|
||||
.expect("Server bundle not found"),
|
||||
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
|
||||
"",
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::extract::Path;
|
||||
|
||||
use axum::http::{HeaderName, HeaderValue};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
@@ -7,10 +7,9 @@ use reqwest::Client;
|
||||
|
||||
const VITE_URL: &str = "http://localhost:3001/vite-server";
|
||||
|
||||
pub async fn vite_reverse_proxy(
|
||||
State(client): State<Client>,
|
||||
Path(path): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
pub async fn vite_reverse_proxy(Path(path): Path<String>) -> impl IntoResponse {
|
||||
let client = Client::new();
|
||||
|
||||
match client.get(format!("{VITE_URL}/{path}")).send().await {
|
||||
Ok(res) => {
|
||||
let mut response_builder = Response::builder().status(res.status().as_u16());
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.11.1"
|
||||
version = "0.15.1"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -14,8 +15,7 @@ include = [
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate_type = ["proc-macro"]
|
||||
proc-marco = true
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0.0", features = ["full"] }
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::utils::{
|
||||
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
|
||||
params_argument, request_argument,
|
||||
};
|
||||
use proc_macro::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat};
|
||||
|
||||
pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
let http_method = parse_macro_input!(attrs as Ident)
|
||||
.to_string()
|
||||
.to_lowercase();
|
||||
|
||||
let api_fn_name = Ident::new(
|
||||
&format!("{}__tuono_internal_api", http_method),
|
||||
Span::call_site().into(),
|
||||
);
|
||||
|
||||
let fn_name = &item.sig.ident;
|
||||
let return_type = &item.sig.output;
|
||||
|
||||
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
|
||||
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
|
||||
|
||||
// Fn Arguments minus the first which always is the request
|
||||
for (i, arg) in item.sig.inputs.iter().enumerate() {
|
||||
if i == 0 {
|
||||
axum_arguments.insert(i, params_argument());
|
||||
continue;
|
||||
}
|
||||
|
||||
if i == 1 {
|
||||
axum_arguments.insert(1, create_struct_fn_arg())
|
||||
}
|
||||
|
||||
if let FnArg::Typed(pat_type) = arg {
|
||||
let index = i - 1;
|
||||
let argument_name = *pat_type.pat.clone();
|
||||
argument_names.insert(index, argument_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
axum_arguments.insert(axum_arguments.len(), request_argument());
|
||||
|
||||
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
|
||||
let application_state_import = import_main_application_state(argument_names.clone());
|
||||
|
||||
quote! {
|
||||
#application_state_import
|
||||
|
||||
#item
|
||||
|
||||
pub async fn #api_fn_name(#axum_arguments)#return_type {
|
||||
|
||||
#application_state_extractor
|
||||
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
|
||||
#fn_name(req.clone(), #argument_names).await
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -1,43 +1,76 @@
|
||||
use crate::utils::{
|
||||
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
|
||||
params_argument, request_argument,
|
||||
};
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, ItemFn};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
|
||||
|
||||
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(item as ItemFn);
|
||||
|
||||
let fn_name = item.clone().sig.ident;
|
||||
let fn_name = &item.sig.ident;
|
||||
|
||||
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
|
||||
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
|
||||
|
||||
// Fn Arguments minus the first which always is the request
|
||||
for (i, arg) in item.sig.inputs.iter().enumerate() {
|
||||
if i == 0 {
|
||||
axum_arguments.insert(i, params_argument());
|
||||
continue;
|
||||
}
|
||||
|
||||
if i == 1 {
|
||||
axum_arguments.insert(1, create_struct_fn_arg())
|
||||
}
|
||||
|
||||
if let FnArg::Typed(pat_type) = arg {
|
||||
let index = i - 1;
|
||||
let argument_name = *pat_type.pat.clone();
|
||||
argument_names.insert(index, argument_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
axum_arguments.insert(axum_arguments.len(), request_argument());
|
||||
|
||||
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
|
||||
let application_state_import = import_main_application_state(argument_names.clone());
|
||||
|
||||
quote! {
|
||||
use tuono_lib::axum::response::IntoResponse;
|
||||
use std::collections::HashMap;
|
||||
use tuono_lib::axum::extract::{State, Path};
|
||||
#application_state_import
|
||||
|
||||
#item
|
||||
|
||||
pub async fn route(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<tuono_lib::reqwest::Client>,
|
||||
request: tuono_lib::axum::extract::Request
|
||||
) -> impl IntoResponse {
|
||||
#axum_arguments
|
||||
) -> impl tuono_lib::axum::response::IntoResponse {
|
||||
|
||||
#application_state_extractor
|
||||
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
|
||||
#fn_name(req.clone(), client).await.render_to_string(req)
|
||||
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
|
||||
}
|
||||
|
||||
pub async fn api(
|
||||
Path(params): Path<HashMap<String, String>>,
|
||||
State(client): State<tuono_lib::reqwest::Client>,
|
||||
request: tuono_lib::axum::extract::Request
|
||||
) -> impl IntoResponse{
|
||||
#axum_arguments
|
||||
) -> impl tuono_lib::axum::response::IntoResponse {
|
||||
|
||||
#application_state_extractor
|
||||
|
||||
let pathname = request.uri();
|
||||
let headers = request.headers();
|
||||
|
||||
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
|
||||
|
||||
#fn_name(req.clone(), client).await.json()
|
||||
#fn_name(req.clone(), #argument_names).await.json()
|
||||
}
|
||||
}
|
||||
.into()
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
mod api;
|
||||
mod handler;
|
||||
mod utils;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
handler::handler_core(args, item)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
|
||||
api::api_core(args, item)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use quote::quote;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Comma;
|
||||
use syn::{parse2, parse_quote, FnArg, Pat, Stmt};
|
||||
|
||||
pub fn create_struct_fn_arg() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let local: Stmt = parse_quote!(
|
||||
use crate::tuono_main_state::ApplicationState;
|
||||
);
|
||||
return Some(local);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
|
||||
if !argument_names.is_empty() {
|
||||
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names, .. } = state;);
|
||||
return Some(use_item);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn params_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
|
||||
std::collections::HashMap<String, String>
|
||||
>
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn request_argument() -> FnArg {
|
||||
parse2(quote! {
|
||||
request: tuono_lib::axum::extract::Request
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
// @ts-expect-error no types are available for this plugin
|
||||
import eslintPluginImport from 'eslint-plugin-import'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
// #region shared
|
||||
'**/build',
|
||||
'**/dist',
|
||||
'**/.tuono',
|
||||
'**/vite.config.ts.timestamp**',
|
||||
// #endregion shared
|
||||
|
||||
// #region package-specific
|
||||
'packages/fs-router-vite-plugin/tests/generator/**',
|
||||
|
||||
'packages/tuono/bin/**',
|
||||
// #endregion package-specific
|
||||
],
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error',
|
||||
},
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
eslintPluginImport.flatConfigs.recommended,
|
||||
eslintPluginImport.flatConfigs.typescript,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
},
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// #region @typescript-eslint
|
||||
'@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 },
|
||||
],
|
||||
// #endregion @typescript-eslint
|
||||
|
||||
// #region import
|
||||
'import/default': 'error',
|
||||
'import/export': 'error',
|
||||
'import/namespace': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-duplicates': 'error',
|
||||
'import/no-named-as-default-member': 'error',
|
||||
'import/no-unused-modules': 'error',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
'newlines-between': 'always-and-inside-groups',
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'object',
|
||||
],
|
||||
},
|
||||
],
|
||||
// #endregion import
|
||||
|
||||
// #region misc
|
||||
/**
|
||||
* @todo some of this might be removed.
|
||||
* They are handled by other plugin and / or typescript
|
||||
*/
|
||||
'no-case-declarations': 'error',
|
||||
'no-empty': 'error',
|
||||
'no-prototype-builtins': 'error',
|
||||
'no-redeclare': 'error',
|
||||
'no-shadow': 'error',
|
||||
'no-undef': 'off',
|
||||
'sort-imports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['apps/documentation/**'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: 'apps/documentation/tsconfig.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||