mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 05:42:47 -07:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cbcd8ad70 | |||
| 9f40a55907 | |||
| 51e01ec142 | |||
| 63fc2b9855 | |||
| edd8a02423 | |||
| a0a81b514c | |||
| 92820804f8 | |||
| e7ea7ed1c4 | |||
| 7471c90f49 | |||
| f545a9c469 | |||
| 30073c226d | |||
| 1b1e91b6c0 | |||
| 71d7ccf27d | |||
| 3ca24ec87f | |||
| b2940d9fa8 | |||
| a7b7f0eac2 | |||
| 4859916edd | |||
| ad3e19e965 | |||
| 03dead1083 | |||
| 341fba6ca9 | |||
| 58c4890122 | |||
| af42e5ef21 | |||
| 5abff0384c | |||
| cc915560b8 | |||
| 9835a4c0d1 | |||
| 90782ae710 | |||
| 75c790dd6e | |||
| 8604858a61 | |||
| d1b6162d3a | |||
| a445644b47 | |||
| ebb4434506 | |||
| 36a53662a0 | |||
| 569d95dc9c | |||
| bc4c4e03f0 | |||
| 170c5ea098 | |||
| 3fd5f166e2 | |||
| a2285a78a1 | |||
| 1ae7f72de3 |
@@ -1,16 +1,11 @@
|
||||
## Context
|
||||
## Context & Description
|
||||
|
||||
<!--
|
||||
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
|
||||
use case that feature fulfills.
|
||||
-->
|
||||
|
||||
## Description
|
||||
|
||||
<!--
|
||||
Thank you for your Pull Request. Please provide a description above and review
|
||||
the requirements below.
|
||||
|
||||
Bug fixes and new features should include tests.
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: Documentation Website CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/**'
|
||||
- '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
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/documentation
|
||||
|
||||
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
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.8.3
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-workspace
|
||||
|
||||
- name: Build project
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/documentation
|
||||
|
||||
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 --ignore-workspace
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Types
|
||||
run: pnpm types
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Deploy documentation website on AWS S3
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/**'
|
||||
- 'apps/documentation/**'
|
||||
|
||||
jobs:
|
||||
deploy-documentation:
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup rust
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install tuono
|
||||
run: cargo install tuono@0.8.3
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --ignore-workspace
|
||||
|
||||
- name: Build project
|
||||
run: tuono build --static
|
||||
|
||||
- name: Deploy
|
||||
uses: reggionick/s3-deploy@v4
|
||||
with:
|
||||
folder: apps/documentation/out/static
|
||||
bucket: tuono-documentation
|
||||
bucket-region: eu-west-3
|
||||
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||
invalidation: /
|
||||
delete-removed: true
|
||||
no-cache: true
|
||||
private: true
|
||||
files-to-include: '{.*/**,**}'
|
||||
@@ -6,6 +6,7 @@ members = [
|
||||
"crates/tuono_lib_macros",
|
||||
]
|
||||
exclude = [
|
||||
"apps/documentation",
|
||||
"examples/mdx",
|
||||
"examples/tuono",
|
||||
"examples/tutorial"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
|
||||
</p>
|
||||
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
|
||||
<div align="center">
|
||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg" />
|
||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg" />
|
||||
</div>
|
||||
[Documentation](https://tuono.dev) | [Tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) |
|
||||
[✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
# Tuono
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
**Tuono** is the react/rust fullstack framework.
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Why Tuono? Just a badass name.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Stadalone apps folder
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"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/ban-types": "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",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.tuono
|
||||
out
|
||||
target
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.9.1"
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Tuono starter
|
||||
|
||||
This is the starter tuono project. To download it run in your terminal:
|
||||
|
||||
```shell
|
||||
$ tuono new [NAME]
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/code-highlight": "^7.11.2",
|
||||
"@mantine/core": "^7.11.2",
|
||||
"@mantine/hooks": "^7.11.2",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@tabler/icons-react": "^3.11.0",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
||||
"@typescript-eslint/parser": "^7.7.1",
|
||||
"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": "^4.6.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,54 @@
|
||||
import { Breadcrumbs, Button } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export default function TuonoBreadcrumbs({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}): 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}
|
||||
>
|
||||
<IconBolt />
|
||||
</Button>
|
||||
{children}
|
||||
</Breadcrumbs>
|
||||
)
|
||||
}
|
||||
|
||||
interface BreadcrumbElementProps {
|
||||
href?: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export function BreadcrumbElement({
|
||||
href,
|
||||
label,
|
||||
}: BreadcrumbElementProps): JSX.Element {
|
||||
if (href) {
|
||||
return (
|
||||
<Button component={Link} href={href} variant="subtle" radius="xl" px={10}>
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button component="span" variant="light" radius="xl">
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Breadcrumbs, { BreadcrumbElement } from './breadcrumbs'
|
||||
|
||||
export default Breadcrumbs
|
||||
|
||||
export { BreadcrumbElement as Element }
|
||||
@@ -0,0 +1,23 @@
|
||||
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'
|
||||
|
||||
export default function EditPage(): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
return (
|
||||
<Button
|
||||
p={0}
|
||||
mt={60}
|
||||
component="a"
|
||||
variant="transparent"
|
||||
leftSection={<IconEdit />}
|
||||
target="_blank"
|
||||
href={GITHUB_URL.concat(pathname).concat('.mdx')}
|
||||
>
|
||||
Edit page
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import EditPage from './edit-page'
|
||||
|
||||
export default EditPage
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
CopyButton,
|
||||
Group,
|
||||
rem,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { IconCopy, IconCheck } from '@tabler/icons-react'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function Hero(): JSX.Element {
|
||||
return (
|
||||
<Container size={1100} mt={200}>
|
||||
<Center>
|
||||
<Title order={1}>The react/rust fullstack framework</Title>
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Text fz="18px">
|
||||
The technologies we love seamessly working together to unleash the
|
||||
<strong> highest web performance</strong> ever met on react
|
||||
</Text>
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Group>
|
||||
<Button component={Link} href="/documentation" size="lg">
|
||||
Get Started
|
||||
</Button>
|
||||
<CopyButton value="cargo install tuono">
|
||||
{({ copied, copy }) => (
|
||||
<Button
|
||||
onClick={copy}
|
||||
size="lg"
|
||||
style={{ border: 'solid 1px var(--mantine-color-violet-1)' }}
|
||||
color="gray"
|
||||
leftSection="cargo install tuono"
|
||||
rightSection={
|
||||
copied ? (
|
||||
<IconCheck style={{ width: rem(20) }} />
|
||||
) : (
|
||||
<IconCopy style={{ width: rem(20) }} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
</Center>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Hero from './hero'
|
||||
|
||||
export default Hero
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxProvider from './mdx-provider'
|
||||
|
||||
export default MdxProvider
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Text, type TextProps } from '@mantine/core'
|
||||
|
||||
export default function MdxBold(props: TextProps): JSX.Element {
|
||||
return <Text fw={700} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxCode from './mdx-code'
|
||||
|
||||
export default MdxCode
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Code } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxCode(
|
||||
props: HTMLAttributes<HTMLPreElement>,
|
||||
): JSX.Element {
|
||||
return <Code {...props} />
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Title } from '@mantine/core'
|
||||
import { useRef, type HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxH2(
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Title
|
||||
{...props}
|
||||
mt={20}
|
||||
order={2}
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxLink from './mdx-link'
|
||||
|
||||
export default MdxLink
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { AnchorHTMLAttributes } from 'react'
|
||||
import { Button } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { IconExternalLink } from '@tabler/icons-react'
|
||||
|
||||
export default function MdxLink(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
{...props}
|
||||
target="_blank"
|
||||
rightSection={
|
||||
<IconExternalLink size="16px" style={{ marginLeft: -5 }} />
|
||||
}
|
||||
variant="transparent"
|
||||
style={{ height: '20px' }}
|
||||
mt={-2}
|
||||
p={0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
component={Link}
|
||||
{...props}
|
||||
target="_blank"
|
||||
variant="transparent"
|
||||
style={{ height: '20px' }}
|
||||
mt={-2}
|
||||
p={0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxPre from './mdx-pre'
|
||||
|
||||
export default MdxPre
|
||||
@@ -0,0 +1,5 @@
|
||||
.pre {
|
||||
code {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { CodeHighlight } from '@mantine/code-highlight'
|
||||
import styles from './mdx-pre.module.css'
|
||||
|
||||
interface PreProps {
|
||||
children: any
|
||||
}
|
||||
export default function MdxPre({ children }: PreProps): JSX.Element {
|
||||
return (
|
||||
<CodeHighlight
|
||||
className={styles.pre}
|
||||
style={{ borderRadius: 8 }}
|
||||
code={children.props.children || ''}
|
||||
language={children.props.className?.replace('language-', '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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'
|
||||
import MdxTitle from './mdx-title'
|
||||
import MdxBold from './mdx-bold/mdx-bold'
|
||||
import MdxH2 from './mdx-h2/mdx-h2'
|
||||
|
||||
interface MdxProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function MdxProvider({
|
||||
children,
|
||||
}: MdxProviderProps): JSX.Element {
|
||||
return (
|
||||
<MDXProvider
|
||||
components={{
|
||||
a: MdxLink,
|
||||
// @ts-expect-error: useless finding the correct props types
|
||||
pre: MdxPre,
|
||||
blockquote: MdxQuote,
|
||||
code: MdxCode,
|
||||
h1: MdxTitle,
|
||||
h2: MdxH2,
|
||||
strong: MdxBold,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MDXProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxQuote from './mdx-quote'
|
||||
|
||||
export default MdxQuote
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Blockquote, Space } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxQuote(
|
||||
props: HTMLAttributes<HTMLQuoteElement>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Blockquote
|
||||
color="violet"
|
||||
py={1}
|
||||
px={20}
|
||||
mt={30}
|
||||
iconSize={30}
|
||||
{...props}
|
||||
style={{ borderRadius: 8 }}
|
||||
/>
|
||||
<Space h="md" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import MdxTitle from './mdx-title'
|
||||
|
||||
export default MdxTitle
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Title } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxTitle(
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<Title
|
||||
{...props}
|
||||
order={1}
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||
import { IconBrandGithub, IconBook } from '@tabler/icons-react'
|
||||
import ThemeBtn from '../theme-btn'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function Actions(): JSX.Element {
|
||||
return (
|
||||
<Flex gap={8}>
|
||||
<Button
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
autoContrast
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<Group gap={8} visibleFrom="sm">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Check the project on github"
|
||||
href="https://github.com/Valerioageno/tuono"
|
||||
target="_blank"
|
||||
component="a"
|
||||
>
|
||||
<IconBrandGithub />
|
||||
</ActionIcon>
|
||||
<ThemeBtn />
|
||||
</Group>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Navbar from './navbar'
|
||||
|
||||
export default Navbar
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import Actions from './actions'
|
||||
|
||||
interface NavbarProps {
|
||||
opened: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
return (
|
||||
<AppShell.Header p="sm">
|
||||
<Flex justify="space-between">
|
||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||
Tuono
|
||||
</Button>
|
||||
<Flex align="center" gap={8}>
|
||||
<Actions />
|
||||
{pathname.startsWith('/documentation') && (
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import Sidebar from './sidebar'
|
||||
|
||||
export default Sidebar
|
||||
@@ -0,0 +1,6 @@
|
||||
.link {
|
||||
border-radius: 8px;
|
||||
margin-top: 0.25rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NavLink, type NavLinkProps } from '@mantine/core'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Link, useRouter } from 'tuono'
|
||||
import { IconChevronRight } from '@tabler/icons-react'
|
||||
|
||||
import styles from './sidebar-link.module.css'
|
||||
|
||||
interface SidebarLinkProps {
|
||||
label: string
|
||||
href: string
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function SidebarLink(
|
||||
props: SidebarLinkProps & NavLinkProps,
|
||||
): JSX.Element {
|
||||
const { pathname } = useRouter()
|
||||
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
|
||||
|
||||
const internalProps = {
|
||||
active: pathname === props.href,
|
||||
className: styles.link,
|
||||
rightSection: props.children && (
|
||||
<IconChevronRight
|
||||
size="1.2rem"
|
||||
stroke={1.5}
|
||||
className="mantine-rotate-rtl"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setIsOpen((state) => !state)
|
||||
}}
|
||||
/>
|
||||
),
|
||||
opened: isOpen,
|
||||
autoContrast: true,
|
||||
...props,
|
||||
}
|
||||
|
||||
if (props.href.startsWith('#')) {
|
||||
return <NavLink component="button" {...internalProps} />
|
||||
}
|
||||
|
||||
return <NavLink component={Link} {...internalProps} />
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { AppShell } from '@mantine/core'
|
||||
import SidebarLink from './sidebar-link'
|
||||
|
||||
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<h3>Tutorial</h3>
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/intro"
|
||||
label="Intro"
|
||||
onClick={close}
|
||||
/>
|
||||
<h3>Documentation</h3>
|
||||
<SidebarLink href="/documentation" label="Overview" onClick={close} />
|
||||
<SidebarLink
|
||||
href="/documentation/installation"
|
||||
label="Installation"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
href="/documentation/getting-started"
|
||||
label="Getting started"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
label="Routing"
|
||||
href="/documentation/routing"
|
||||
defaultOpened
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/routing/intro"
|
||||
label="Project structure"
|
||||
onClick={close}
|
||||
/>
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
label="Contributing"
|
||||
href="/documentation/contributing"
|
||||
onClick={close}
|
||||
leftSection="✨"
|
||||
/>
|
||||
</AppShell.Navbar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import ThemeBtn from './theme-btn'
|
||||
|
||||
export default ThemeBtn
|
||||
@@ -0,0 +1,24 @@
|
||||
.icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@mixin dark {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.light {
|
||||
@mixin light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
useMantineColorScheme,
|
||||
useComputedColorScheme,
|
||||
} from '@mantine/core'
|
||||
import { IconSun, IconMoon } from '@tabler/icons-react'
|
||||
import cx from 'clsx'
|
||||
|
||||
import classes from './theme-btn.module.css'
|
||||
|
||||
export default function ThemeBtn(): JSX.Element {
|
||||
const { setColorScheme } = useMantineColorScheme()
|
||||
const computedColorScheme = useComputedColorScheme('light', {
|
||||
getInitialValueInEffect: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
|
||||
}
|
||||
variant="default"
|
||||
size="lg"
|
||||
aria-label="Toggle color scheme"
|
||||
>
|
||||
<IconSun className={cx(classes.icon, classes.light)} stroke={1.5} />
|
||||
<IconMoon className={cx(classes.icon, classes.dark)} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// declaration.d.ts
|
||||
declare module '*.css'
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
createTheme,
|
||||
MantineProvider,
|
||||
AppShell,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { Head, useRouter } from 'tuono'
|
||||
import Navbar from '../components/navbar'
|
||||
|
||||
import '@mantine/core/styles.css'
|
||||
import '@mantine/code-highlight/styles.css'
|
||||
import Sidebar from '../components/sidebar'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const theme = createTheme({
|
||||
primaryColor: 'violet',
|
||||
primaryShade: { light: 6, dark: 9 },
|
||||
fontFamily: 'Roboto',
|
||||
respectReducedMotion: true,
|
||||
fontSizes: {
|
||||
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
xs: '16px',
|
||||
sm: '16px',
|
||||
},
|
||||
colors: {
|
||||
dark: [
|
||||
'#d5d7e0',
|
||||
'#acaebf',
|
||||
'#8c8fa3',
|
||||
'#666980',
|
||||
'#4d4f66',
|
||||
'#34354a',
|
||||
'#2b2c3d',
|
||||
'#1d1e30',
|
||||
'#0c0d21',
|
||||
'#01010a',
|
||||
],
|
||||
},
|
||||
headings: {
|
||||
sizes: {
|
||||
h1: {
|
||||
fontSize: '48px',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<ColorSchemeScript />
|
||||
</Head>
|
||||
<MantineProvider theme={theme}>
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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 component="article" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Contributing</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="✨ Contributing" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Contributing
|
||||
|
||||
## TL;DR
|
||||
|
||||
The project is massive - if you like it do consider to contribute!
|
||||
|
||||
## Getting started
|
||||
|
||||
The `tuono` project can mostly be split by the following sub-domains:
|
||||
|
||||
- The CLI
|
||||
- The rust backend
|
||||
- The react frontend
|
||||
- The documentation website (which is written with tuono 🚀)
|
||||
|
||||
To check what are the knowledge requirement for each domain check the
|
||||
[requirements](#requirements) section below.
|
||||
|
||||
Currently I'm keeping a private dashboard to prioritize new features and bug fixes but if you
|
||||
want to propose something please open a new issue on github or reach me using
|
||||
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
|
||||
on twitter (X) DMs `@valerioageno`, [linkedin](https://www.linkedin.com/in/valerioageno)
|
||||
and discord `@__v__v__`.
|
||||
|
||||
## Requirements
|
||||
|
||||
It's not strictly required to know both React (& typescript) and Rust (even though it
|
||||
would be a great nice to have).
|
||||
|
||||
Without taking into account specific cases we can mostly split the domain requirements by:
|
||||
|
||||
- The `CLI` needs Rust knowledge (even though a couple of scenario might need also 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).
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Getting started</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Getting started" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Getting started
|
||||
|
||||
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
|
||||
|
||||
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
|
||||
> `tuono` CLI yet.
|
||||
|
||||
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 folder).
|
||||
|
||||
Then to run the local development environment run inside the project folder `tuono dev`
|
||||
|
||||
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
|
||||
|
||||
Now to execute it just run `cargo run --release`.
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Documentation</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs></Breadcrumbs>
|
||||
|
||||
# Tuono
|
||||
|
||||
**Tuono is a fullstack framework for building React applications using Rust as
|
||||
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 fullstack right? (no)**
|
||||
|
||||
Tuono is a fullstack React framework with the server side written in Rust.
|
||||
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
|
||||
React is still React - it is just superpowered.
|
||||
|
||||
**Rust is an 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 with 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 thread backend
|
||||
- ⌨️ MDX support
|
||||
- ⚙️ Build optimizations
|
||||
- Custom APIs\*
|
||||
- Image optimization\*
|
||||
- Server streamed content\*
|
||||
|
||||
> \*development in progress
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Head } from 'tuono'
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Installation</title>
|
||||
</Head>
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Installation" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
`Tuono` is a development environment built in rust and typescript that outputs a website written with both the
|
||||
languages. Since that you need the following tools installed on your computer to work with it:
|
||||
|
||||
- `Rust` - The rust programming language toolchain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
|
||||
- `Cargo` - The rust package manager
|
||||
- `NodeJs` - The Javascript runtime (Go [here](https://nodejs.org/en/download/package-manager) for installing both `nodejs` and `npm`)
|
||||
- `npm` or `yarn` or `pnpm` - A javascript package manager
|
||||
|
||||
> NodeJs is needed just for the development environment. The final output will run on a rust server.
|
||||
|
||||
## Installation
|
||||
|
||||
The tuono `CLI` is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
|
||||
|
||||
```bash
|
||||
cargo install tuono
|
||||
```
|
||||
|
||||
To check that is correctly installed run:
|
||||
|
||||
```bash
|
||||
tuono --version
|
||||
```
|
||||
|
||||
Run `tuono -h` to see all the available commands.
|
||||
|
||||
```bash
|
||||
The react/rust fullstack framework
|
||||
|
||||
Usage: tuono <COMMAND>
|
||||
|
||||
Commands:
|
||||
dev Start the development environment
|
||||
build Build the production assets
|
||||
new Scaffold a new project
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Routing</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Routing" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Routing index
|
||||
|
||||
TODO
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Routing</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Routing" href="/documentation/routing" />
|
||||
<Element label="Project structure" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Project structure
|
||||
|
||||
Todo
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Tutorial intro</title>
|
||||
</Head>
|
||||
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Tutorial" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Tutorial
|
||||
|
||||
You can find the tutorial [here](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
||||
|
||||
Other tasks have the priority right now. The tutorial will be ported here as soon as possible.
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Head } from 'tuono'
|
||||
import Hero from '../components/hero'
|
||||
|
||||
export default function IndexPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Tuono - The react/rust fullstack framework</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="The technologies we love seamessly working together to unleash the highest web performance ever met on react"
|
||||
/>
|
||||
</Head>
|
||||
<Hero />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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 */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.8.2"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -63,6 +63,7 @@ impl App {
|
||||
.unwrap()
|
||||
.replace(&format!("{base_path_str}/src/routes"), "")
|
||||
.replace(".rs", "")
|
||||
.replace(".mdx", "")
|
||||
.replace(".tsx", "");
|
||||
|
||||
if entry.extension().unwrap() == "rs" {
|
||||
|
||||
@@ -104,9 +104,9 @@ pub fn app() -> std::io::Result<()> {
|
||||
sleep(Duration::from_secs(1))
|
||||
}
|
||||
|
||||
app.route_map
|
||||
.iter()
|
||||
.for_each(|(_, route)| route.save_ssg_html(&reqwest));
|
||||
for (_, route) in app.route_map {
|
||||
route.save_ssg_html(&reqwest)
|
||||
}
|
||||
|
||||
// Close server
|
||||
let _ = rust_server.kill();
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Route {
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
let file_path = PathBuf::from(format!("out/static{}.html", &self.path));
|
||||
let file_path = self.html_file_path();
|
||||
|
||||
let parent_dir = file_path.parent().unwrap();
|
||||
|
||||
@@ -123,6 +123,11 @@ impl Route {
|
||||
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
|
||||
}
|
||||
}
|
||||
|
||||
fn html_file_path(&self) -> PathBuf {
|
||||
let cleaned_path = self.path.replace("index", "");
|
||||
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -162,4 +167,22 @@ mod tests {
|
||||
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_define_the_correct_html_build_path() {
|
||||
let routes = [
|
||||
("/index", "out/static/index.html"),
|
||||
("/documentation", "out/static/documentation/index.html"),
|
||||
(
|
||||
"/documentation/routing",
|
||||
"out/static/documentation/routing/index.html",
|
||||
),
|
||||
];
|
||||
|
||||
for (path, html) in routes {
|
||||
let route = Route::new(path.to_string());
|
||||
|
||||
assert_eq!(route.html_file_path(), PathBuf::from(html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.8.2"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
@@ -32,7 +32,7 @@ regex = "1.10.5"
|
||||
either = "1.13.0"
|
||||
tower-http = {version = "0.5.2", features = ["fs"]}
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.8.2"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.9.2"}
|
||||
# Match the same version used by axum
|
||||
tokio-tungstenite = "0.21.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib_macros"
|
||||
version = "0.8.2"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
|
||||
+9
-8
@@ -2,19 +2,20 @@
|
||||
"name": "workspace",
|
||||
"packageManager": "pnpm@9.1.1",
|
||||
"scripts": {
|
||||
"dev": "turbo dev --filter tuono",
|
||||
"dev": "turbo watch dev",
|
||||
"build": "turbo build --filter tuono",
|
||||
"lint": "turbo lint --filter tuono",
|
||||
"format": "turbo format --filter tuono",
|
||||
"format:check": "turbo format:check --filter tuono",
|
||||
"lint": "turbo lint",
|
||||
"format": "turbo format",
|
||||
"format:check": "turbo format:check",
|
||||
"types": "turbo types --filter tuono",
|
||||
"test": "turbo test",
|
||||
"test:watch": "turbo test:watch"
|
||||
"test:watch": "turbo test:watch"
|
||||
},
|
||||
"workspaces": [
|
||||
"tuono",
|
||||
"tuono-lazy-fn-vite-plugin",
|
||||
"tuono-fs-router-vite-plugin"
|
||||
"tuono-lazy-fn-vite-plugin",
|
||||
"tuono-fs-router-vite-plugin",
|
||||
"tuono-router"
|
||||
],
|
||||
"author": "Valerio Ageno",
|
||||
"license": "MIT",
|
||||
@@ -37,6 +38,6 @@
|
||||
"vitest": "^1.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"turbo": "^2.0.4"
|
||||
"turbo": "^2.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-fs-router-vite-plugin",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.2",
|
||||
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-lazy-fn-vite-plugin",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.2",
|
||||
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const TUONO_DYNAMIC_FN_ID = 'dynamic'
|
||||
export const REACT_LAZY_FN_ID = 'lazy'
|
||||
export const TUONO_LAZY_FN_ID = 'lazyLoadComponent'
|
||||
export const TUONO_MAIN_PACKAGE = 'tuono'
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { PluginItem } from '@babel/core'
|
||||
import {
|
||||
TUONO_MAIN_PACKAGE,
|
||||
TUONO_DYNAMIC_FN_ID,
|
||||
REACT_LAZY_FN_ID,
|
||||
TUONO_LAZY_FN_ID,
|
||||
} from './constants'
|
||||
|
||||
import * as t from '@babel/types'
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
} from '@babel/types'
|
||||
|
||||
/**
|
||||
* [SERVER build]
|
||||
* This plugin just removes the `dynamic` imported function from any tuono import
|
||||
*/
|
||||
const RemoveTuonoLazyImport: PluginItem = {
|
||||
@@ -38,49 +39,28 @@ const RemoveTuonoLazyImport: PluginItem = {
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin adds: "Import { lazy } from 'react'"
|
||||
* and translate dynamic call into a React.lazy call
|
||||
* [CLIENT build]
|
||||
* This plugin replace the `dynamic` function with the `lazyLoadComponent` one
|
||||
*/
|
||||
const ImportReactLazy: PluginItem = {
|
||||
name: 'import-react-lazy-plugin',
|
||||
const ReplaceTuonoLazyImport: PluginItem = {
|
||||
name: 'remove-tuono-lazy-import-plugin',
|
||||
visitor: {
|
||||
// Add the import statement
|
||||
Program: (path: any) => {
|
||||
let isReactImported = false
|
||||
|
||||
path.node.body.forEach((val: any) => {
|
||||
if (val.type === 'ImportDeclaration' && val.source.value === 'react') {
|
||||
isReactImported = true
|
||||
// TODO: Handle also here case of already imported react
|
||||
// Right now works just for the main routes file
|
||||
ImportSpecifier: (path) => {
|
||||
if ((path.node.imported as Identifier).name === TUONO_DYNAMIC_FN_ID) {
|
||||
if (
|
||||
(path.parentPath.node as ImportDeclaration).source.value ===
|
||||
TUONO_MAIN_PACKAGE
|
||||
) {
|
||||
;(path.node.imported as Identifier).name = TUONO_LAZY_FN_ID
|
||||
}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!isReactImported) {
|
||||
const importDeclaration = t.importDeclaration(
|
||||
[
|
||||
t.importSpecifier(
|
||||
t.identifier(REACT_LAZY_FN_ID),
|
||||
t.identifier(REACT_LAZY_FN_ID),
|
||||
),
|
||||
],
|
||||
t.stringLiteral('react'),
|
||||
)
|
||||
path.unshiftContainer('body', importDeclaration)
|
||||
}
|
||||
},
|
||||
// Update lazy function name from `dynamic` to `lazy`
|
||||
CallExpression: (path: any) => {
|
||||
if (path.node.callee?.name === TUONO_DYNAMIC_FN_ID) {
|
||||
path.node.callee.name = REACT_LAZY_FN_ID
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* For the server side we need to statically import the lazy loaded components
|
||||
* [SERVER build]
|
||||
* This plugin statically imports the lazy loaded components
|
||||
*/
|
||||
const TurnLazyIntoStaticImport: PluginItem = {
|
||||
name: 'turn-lazy-into-static-import-plugin',
|
||||
@@ -124,8 +104,8 @@ export function LazyLoadingPlugin(): Plugin {
|
||||
plugins: [
|
||||
['@babel/plugin-syntax-jsx', {}],
|
||||
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
||||
[RemoveTuonoLazyImport],
|
||||
[!opts?.ssr ? ImportReactLazy : []],
|
||||
[!opts?.ssr ? ReplaceTuonoLazyImport : []],
|
||||
[opts?.ssr ? RemoveTuonoLazyImport : []],
|
||||
[opts?.ssr ? TurnLazyIntoStaticImport : []],
|
||||
],
|
||||
sourceMaps: true,
|
||||
|
||||
@@ -10,10 +10,9 @@ const PokemonspokemonImport = dynamic(
|
||||
)
|
||||
`
|
||||
|
||||
const CLIENT_RESULT = `import { lazy } from "react";
|
||||
import { createRoute } from 'tuono';
|
||||
const IndexImport = lazy(() => import('./../src/routes/index'));
|
||||
const PokemonspokemonImport = lazy(() => import('./../src/routes/pokemons/[pokemon]'));`
|
||||
const CLIENT_RESULT = `import { createRoute, lazyLoadComponent as dynamic } from 'tuono';
|
||||
const IndexImport = dynamic(() => import('./../src/routes/index'));
|
||||
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));`
|
||||
|
||||
const SERVER_RESULT = `import { createRoute } from 'tuono';
|
||||
import IndexImport from "./../src/routes/index";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
# tuono-router
|
||||
|
||||
This package holds the logic related to the client/server react routing.
|
||||
|
||||
Check [tuono](https://github.com/Valerioageno/tuono) for more.
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "0.9.2",
|
||||
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
"build": "vite build",
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||
"types": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Valerio Ageno",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/esm/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.cts",
|
||||
"default": "./dist/cjs/index.cjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-intersection-observer": "^9.13.0",
|
||||
"vite": "^5.2.11",
|
||||
"zustand": "4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/config": "^0.7.11",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
"vitest": "^1.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react'
|
||||
import { useRouter } from '../hooks/useRouter'
|
||||
import type { AnchorHTMLAttributes, MouseEvent } from 'react'
|
||||
import useRoute from '../hooks/useRoute'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
interface TuonoLinkProps {
|
||||
preload?: boolean
|
||||
}
|
||||
|
||||
export default function Link(
|
||||
componentProps: AnchorHTMLAttributes<HTMLAnchorElement> & TuonoLinkProps,
|
||||
): JSX.Element {
|
||||
const { preload = true, ...props } = componentProps
|
||||
const router = useRouter()
|
||||
const route = useRoute(props.href)
|
||||
const { ref } = useInView({
|
||||
onChange(inView) {
|
||||
if (inView && preload) route?.component.preload()
|
||||
},
|
||||
triggerOnce: true,
|
||||
})
|
||||
|
||||
const handleTransition = (e: MouseEvent<HTMLAnchorElement>): void => {
|
||||
e.preventDefault()
|
||||
props.onClick?.(e)
|
||||
|
||||
if (props.href?.startsWith('#')) {
|
||||
window.location.hash = props.href
|
||||
return
|
||||
}
|
||||
|
||||
router.push(props.href || '')
|
||||
}
|
||||
|
||||
return (
|
||||
<a {...props} ref={ref} onClick={handleTransition}>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react'
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import NotFound from './NotFound'
|
||||
import useRoute from '../hooks/useRoute'
|
||||
|
||||
interface MatchesProps {
|
||||
// user defined props
|
||||
serverSideProps: any
|
||||
}
|
||||
|
||||
export function Matches({ serverSideProps }: MatchesProps): JSX.Element {
|
||||
const location = useRouterStore((st) => st.location)
|
||||
|
||||
const route = useRoute(location.pathname)
|
||||
|
||||
if (!route) {
|
||||
return <NotFound />
|
||||
}
|
||||
|
||||
return <RouteMatch route={route} serverSideProps={serverSideProps} />
|
||||
}
|
||||
+3
-3
@@ -14,7 +14,7 @@ const root = {
|
||||
component: ({ children }: Props) => (
|
||||
<div data-testid="root">root route {children}</div>
|
||||
),
|
||||
} as Route
|
||||
} as unknown as Route
|
||||
|
||||
const parent = {
|
||||
component: ({ children }: Props) => (
|
||||
@@ -23,14 +23,14 @@ const parent = {
|
||||
options: {
|
||||
getParentRoute: () => root,
|
||||
},
|
||||
} as Route
|
||||
} as unknown as Route
|
||||
|
||||
const route = {
|
||||
component: () => <p data-testid="route">current route</p>,
|
||||
options: {
|
||||
getParentRoute: () => parent,
|
||||
},
|
||||
} as Route
|
||||
} as unknown as Route
|
||||
|
||||
describe('Test RouteMatch component', () => {
|
||||
afterEach(() => {
|
||||
@@ -0,0 +1,96 @@
|
||||
import * as React from 'react'
|
||||
import type { Route } from '../route'
|
||||
import { useServerSideProps } from '../hooks/useServerSideProps'
|
||||
|
||||
interface MatchProps {
|
||||
route: Route
|
||||
// User defined server side props
|
||||
serverSideProps: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the route match with the root element if exists
|
||||
*
|
||||
* It handles the fetch of the client side resources
|
||||
*/
|
||||
export const RouteMatch = ({
|
||||
route,
|
||||
serverSideProps,
|
||||
}: MatchProps): JSX.Element => {
|
||||
const { data, isLoading } = useServerSideProps(route, serverSideProps)
|
||||
|
||||
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
|
||||
|
||||
return (
|
||||
<TraverseRootComponents routes={routes} data={data} isLoading={isLoading}>
|
||||
<route.component data={data} isLoading={isLoading} />
|
||||
</TraverseRootComponents>
|
||||
)
|
||||
}
|
||||
|
||||
interface TraverseRootComponentsProps {
|
||||
routes: Route[]
|
||||
data: any
|
||||
isLoading: boolean
|
||||
children?: React.ReactNode
|
||||
index?: number
|
||||
}
|
||||
|
||||
interface ParentProps {
|
||||
children: React.ReactNode
|
||||
data: any
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
* This component traverses and renders
|
||||
* all the components that wraps the selected route (__root).
|
||||
* The parents components need to be memoized in order to avoid
|
||||
* re-rendering bugs when changing route.
|
||||
*/
|
||||
const TraverseRootComponents = React.memo(
|
||||
({
|
||||
routes,
|
||||
data,
|
||||
isLoading,
|
||||
index = 0,
|
||||
children,
|
||||
}: TraverseRootComponentsProps): JSX.Element => {
|
||||
if (routes.length > index) {
|
||||
const Parent = React.useMemo(
|
||||
() =>
|
||||
routes[index]?.component as unknown as (
|
||||
props: ParentProps,
|
||||
) => JSX.Element,
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<Parent data={data} isLoading={isLoading}>
|
||||
<TraverseRootComponents
|
||||
routes={routes}
|
||||
data={data}
|
||||
isLoading={isLoading}
|
||||
index={index + 1}
|
||||
>
|
||||
{children}
|
||||
</TraverseRootComponents>
|
||||
</Parent>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
},
|
||||
)
|
||||
|
||||
const loadParentComponents = (route: Route, loader: Route[] = []): Route[] => {
|
||||
const parentComponent = route.options?.getParentRoute?.()
|
||||
|
||||
loader.push(parentComponent)
|
||||
|
||||
if (!parentComponent.isRoot) {
|
||||
return loadParentComponents(parentComponent, loader)
|
||||
}
|
||||
|
||||
return loader.reverse()
|
||||
}
|
||||
+1
-5
@@ -33,12 +33,8 @@ function RouterContextProvider({
|
||||
|
||||
const routerContext = getRouterContext()
|
||||
|
||||
const pendingElement = router.options.defaultPendingComponent ? (
|
||||
<router.options.defaultPendingComponent />
|
||||
) : null
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={pendingElement}>
|
||||
<React.Suspense>
|
||||
<routerContext.Provider value={router}>{children}</routerContext.Provider>
|
||||
</React.Suspense>
|
||||
)
|
||||
@@ -1,4 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import type { RouteComponent } from './types'
|
||||
|
||||
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
|
||||
|
||||
/**
|
||||
* Helper function to lazy load any component.
|
||||
@@ -9,7 +12,7 @@ import * as React from 'react'
|
||||
* It can be wrapped within a React.Suspense component in order to handle its loading state.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const dynamic = (importFn: () => JSX.Element): JSX.Element => {
|
||||
export const dynamic = (importFn: ImportFn): JSX.Element => {
|
||||
/**
|
||||
*
|
||||
* This function is just a placeholder. The real work is done by the bundler.
|
||||
@@ -35,3 +38,9 @@ export const dynamic = (importFn: () => JSX.Element): JSX.Element => {
|
||||
*/
|
||||
return <></>
|
||||
}
|
||||
|
||||
export const lazyLoadComponent = (factory: ImportFn): RouteComponent => {
|
||||
const Component = React.lazy(factory) as unknown as RouteComponent
|
||||
Component.preload = factory
|
||||
return Component
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||
import useRoute from './useRoute'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
|
||||
describe('Test useRoute fn', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
test('match routes by ids', () => {
|
||||
vi.mock('./useInternalRouter.tsx', () => ({
|
||||
useInternalRouter: (): { routesById: Record<string, any> } => {
|
||||
return {
|
||||
routesById: {
|
||||
'/': { id: '/' },
|
||||
'/about': { id: '/about' },
|
||||
'/posts': { id: '/posts' }, // posts/index
|
||||
'/posts/[post]': { id: '/posts/[post]' },
|
||||
'/posts/defined-post': { id: '/posts/defined-post' },
|
||||
'/posts/[post]/[comment]': { id: '/posts/[post]/[comment]' },
|
||||
},
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
expect(useRoute('/')?.id).toBe('/')
|
||||
expect(useRoute('/not-found')?.id).toBe(undefined)
|
||||
expect(useRoute('/about')?.id).toBe('/about')
|
||||
expect(useRoute('/posts/')?.id).toBe('/posts')
|
||||
expect(useRoute('/posts/dynamic-post')?.id).toBe('/posts/[post]')
|
||||
expect(useRoute('/posts/defined-post')?.id).toBe('/posts/defined-post')
|
||||
expect(useRoute('/posts/dynamic-post/dynamic-comment')?.id).toBe(
|
||||
'/posts/[post]/[comment]',
|
||||
)
|
||||
})
|
||||
})
|
||||
+19
-24
@@ -1,26 +1,33 @@
|
||||
import * as React from 'react'
|
||||
import { useInternalRouter } from '../hooks/useInternalRouter'
|
||||
import { useRouterStore } from '../hooks/useRouterStore'
|
||||
import type { Route } from '../route'
|
||||
import { RouteMatch } from './RouteMatch'
|
||||
import NotFound from './NotFound'
|
||||
|
||||
interface MatchesProps {
|
||||
// user defined props
|
||||
serverSideProps: any
|
||||
}
|
||||
import { useInternalRouter } from './useInternalRouter'
|
||||
|
||||
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
|
||||
|
||||
/**
|
||||
* In order to correctly handle pathnames that might finish with a slash
|
||||
* we first sanitize them by removing the final slash.
|
||||
*/
|
||||
export function sanitizePathname(pathname: string): string {
|
||||
if (pathname.endsWith('/') && pathname !== '/') {
|
||||
return pathname.substring(0, pathname.length - 1)
|
||||
}
|
||||
|
||||
return pathname
|
||||
}
|
||||
|
||||
/*
|
||||
* This function is also implemented on server side to match the bundle
|
||||
* This hook is also implemented on server side to match the bundle
|
||||
* file to load at the first rendering.
|
||||
*
|
||||
* File: crates/tuono_lib/src/payload.rs
|
||||
*
|
||||
* Optimizations should occour on both
|
||||
*/
|
||||
export function getRouteByPathname(pathname: string): Route | undefined {
|
||||
export default function useRoute(pathname?: string): Route | undefined {
|
||||
if (!pathname) return
|
||||
|
||||
pathname = sanitizePathname(pathname)
|
||||
|
||||
const { routesById } = useInternalRouter()
|
||||
|
||||
if (routesById[pathname]) return routesById[pathname]
|
||||
@@ -61,15 +68,3 @@ export function getRouteByPathname(pathname: string): Route | undefined {
|
||||
if (!match) return
|
||||
return routesById[match]
|
||||
}
|
||||
|
||||
export function Matches({ serverSideProps }: MatchesProps): JSX.Element {
|
||||
const location = useRouterStore((st) => st.location)
|
||||
|
||||
const route = getRouteByPathname(location.pathname)
|
||||
|
||||
if (!route) {
|
||||
return <NotFound />
|
||||
}
|
||||
|
||||
return <RouteMatch route={route} serverSideProps={serverSideProps} />
|
||||
}
|
||||
@@ -2,5 +2,5 @@ export { RouterProvider } from './components/RouterProvider'
|
||||
export { default as Link } from './components/Link'
|
||||
export { createRouter } from './router'
|
||||
export { createRoute, createRootRoute } from './route'
|
||||
export { dynamic } from './dynamic'
|
||||
export { dynamic, lazyLoadComponent } from './dynamic'
|
||||
export { useRouter } from './hooks/useRouter'
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { RouterType } from './router'
|
||||
import type { RouteComponent } from './types'
|
||||
import { trimPathLeft, joinPaths } from './utils'
|
||||
|
||||
interface RouteOptions {
|
||||
isRoot?: boolean
|
||||
getParentRoute?: () => Route
|
||||
path?: string
|
||||
component: () => JSX.Element
|
||||
component: RouteComponent
|
||||
}
|
||||
|
||||
export function createRoute(options: RouteOptions): Route {
|
||||
@@ -25,7 +26,7 @@ export class Route {
|
||||
router: RouterType
|
||||
isRoot: boolean
|
||||
originalIndex?: number
|
||||
component: () => JSX.Element
|
||||
component: RouteComponent
|
||||
|
||||
constructor(options: RouteOptions) {
|
||||
this.isRoot = options.isRoot ?? typeof options.getParentRoute !== 'function'
|
||||
@@ -11,3 +11,12 @@ export interface ServerProps {
|
||||
}
|
||||
props: any
|
||||
}
|
||||
|
||||
export interface RouteProps {
|
||||
data: any
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export type RouteComponent = ((props: RouteProps) => JSX.Element) & {
|
||||
preload: () => void
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": ["src", "tests", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="vite/client" />
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import { tanstackBuildConfig } from '@tanstack/config/build'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
const config = defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
name: 'react-router',
|
||||
watch: true,
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default mergeConfig(
|
||||
config,
|
||||
tanstackBuildConfig({
|
||||
entry: './src/index.ts',
|
||||
srcDir: './src',
|
||||
}),
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.8.2",
|
||||
"version": "0.9.2",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
@@ -8,9 +8,7 @@
|
||||
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
|
||||
"format": "prettier -u --write --ignore-unknown '**/*'",
|
||||
"format:check": "prettier --check --ignore-unknown '**/*'",
|
||||
"types": "tsc --noEmit",
|
||||
"test:watch": "vitest",
|
||||
"test": "vitest run"
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
@@ -92,17 +90,14 @@
|
||||
"react-meta-tags": "^1.0.1",
|
||||
"tuono-fs-router-vite-plugin": "workspace:*",
|
||||
"tuono-lazy-fn-vite-plugin": "workspace:*",
|
||||
"vite": "^5.2.11",
|
||||
"zustand": "4.4.7"
|
||||
"tuono-router": "workspace:*",
|
||||
"vite": "^5.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/babel-traverse": "^6.25.10",
|
||||
"@types/babel__traverse": "^7.20.6",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.2.4",
|
||||
"vitest": "^1.5.2"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { hydrateRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createRouter } from '../router'
|
||||
import { RouterProvider, createRouter } from 'tuono-router'
|
||||
|
||||
type RouteTree = any
|
||||
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import Head from 'react-meta-tags'
|
||||
import {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
dynamic,
|
||||
useRouter,
|
||||
} from './router'
|
||||
|
||||
export type { TuonoProps } from './types'
|
||||
|
||||
export {
|
||||
createRoute,
|
||||
@@ -18,6 +7,10 @@ export {
|
||||
Link,
|
||||
RouterProvider,
|
||||
dynamic,
|
||||
lazyLoadComponent,
|
||||
useRouter,
|
||||
Head,
|
||||
}
|
||||
} from 'tuono-router'
|
||||
|
||||
export { Head }
|
||||
|
||||
export type { TuonoProps } from './types'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user