mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
24 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 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -8,6 +8,6 @@ name = "tuono"
|
||||
path = ".tuono/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tuono_lib = "0.8.3"
|
||||
tuono_lib = "0.9.1"
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"tuono": "0.8.3"
|
||||
"tuono": "0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdx": "^2.0.13",
|
||||
|
||||
@@ -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 }
|
||||
@@ -25,7 +25,7 @@ export default function Hero(): JSX.Element {
|
||||
</Center>
|
||||
<Center mt={50}>
|
||||
<Group>
|
||||
<Button component={Link} href="/documentation/installation" size="lg">
|
||||
<Button component={Link} href="/documentation" size="lg">
|
||||
Get Started
|
||||
</Button>
|
||||
<CopyButton value="cargo install tuono">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Text, type TextProps } from '@mantine/core'
|
||||
|
||||
export default function MdxBold(props: TextProps): JSX.Element {
|
||||
return <Text fw={700} {...props} />
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Title } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import { useRef, type HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxH2(
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
): JSX.Element {
|
||||
return <Title {...props} mt={20} order={2} />
|
||||
return (
|
||||
<Title
|
||||
{...props}
|
||||
mt={20}
|
||||
order={2}
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IconExternalLink } from '@tabler/icons-react'
|
||||
export default function MdxLink(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
): JSX.Element {
|
||||
if (props.href?.startsWith('http')) {
|
||||
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
@@ -27,7 +27,6 @@ export default function MdxLink(
|
||||
component={Link}
|
||||
{...props}
|
||||
target="_blank"
|
||||
rightSection={<IconExternalLink size="16px" style={{ marginLeft: -5 }} />}
|
||||
variant="transparent"
|
||||
style={{ height: '20px' }}
|
||||
mt={-2}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 {
|
||||
@@ -25,6 +26,7 @@ export default function MdxProvider({
|
||||
code: MdxCode,
|
||||
h1: MdxTitle,
|
||||
h2: MdxH2,
|
||||
strong: MdxBold,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function MdxQuote(
|
||||
<>
|
||||
<Blockquote
|
||||
color="violet"
|
||||
p={8}
|
||||
py={1}
|
||||
px={20}
|
||||
mt={30}
|
||||
iconSize={30}
|
||||
|
||||
@@ -4,5 +4,13 @@ import type { HTMLAttributes } from 'react'
|
||||
export default function MdxTitle(
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
): JSX.Element {
|
||||
return <Title {...props} order={1} />
|
||||
return (
|
||||
<Title
|
||||
{...props}
|
||||
order={1}
|
||||
id={String(props.children ?? '')
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, Button, ActionIcon } from '@mantine/core'
|
||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
||||
import { IconBrandGithub, IconBook } from '@tabler/icons-react'
|
||||
import ThemeBtn from '../theme-btn'
|
||||
import { Link } from 'tuono'
|
||||
@@ -7,7 +7,7 @@ export default function Actions(): JSX.Element {
|
||||
return (
|
||||
<Flex gap={8}>
|
||||
<Button
|
||||
href="/documentation/installation"
|
||||
href="/documentation"
|
||||
component={Link}
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
@@ -15,17 +15,19 @@ export default function Actions(): JSX.Element {
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,11 @@ export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
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}
|
||||
@@ -21,11 +25,7 @@ export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||
Tuono
|
||||
</Button>
|
||||
</Flex>
|
||||
<Actions />
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
)
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
@@ -1,32 +1,44 @@
|
||||
import { AppShell, NavLink } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
import { AppShell } from '@mantine/core'
|
||||
import SidebarLink from './sidebar-link'
|
||||
|
||||
export default function Sidebar(): JSX.Element {
|
||||
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<h3>Tutorial</h3>
|
||||
<NavLink
|
||||
<SidebarLink
|
||||
href="/documentation/tutorial/intro"
|
||||
component={Link}
|
||||
label="Intro"
|
||||
onClick={close}
|
||||
/>
|
||||
<h3>Documentation</h3>
|
||||
<NavLink
|
||||
<SidebarLink href="/documentation" label="Overview" onClick={close} />
|
||||
<SidebarLink
|
||||
href="/documentation/installation"
|
||||
component={Link}
|
||||
label="Installation"
|
||||
onClick={close}
|
||||
/>
|
||||
<NavLink label="Routing" href="#required-for-focus" defaultOpened>
|
||||
<NavLink
|
||||
<SidebarLink
|
||||
href="/documentation/getting-started"
|
||||
label="Getting started"
|
||||
onClick={close}
|
||||
/>
|
||||
<SidebarLink
|
||||
label="Routing"
|
||||
href="/documentation/routing"
|
||||
defaultOpened
|
||||
onClick={close}
|
||||
>
|
||||
<SidebarLink
|
||||
href="/documentation/routing/intro"
|
||||
component={Link}
|
||||
label="FS routing"
|
||||
label="Project structure"
|
||||
onClick={close}
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
</SidebarLink>
|
||||
<SidebarLink
|
||||
label="Contributing"
|
||||
component={Link}
|
||||
href="/documentation/contributing"
|
||||
onClick={close}
|
||||
leftSection="✨"
|
||||
/>
|
||||
</AppShell.Navbar>
|
||||
)
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
AppShell,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { Head } from 'tuono'
|
||||
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
|
||||
@@ -52,6 +53,8 @@ const theme = createTheme({
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
const [opened, { toggle }] = useDisclosure()
|
||||
|
||||
const { pathname } = useRouter()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -67,6 +70,7 @@ export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { AppShell, Container } from '@mantine/core'
|
||||
import MdxProvider from '../../components/mdx-provider'
|
||||
import Sidebar from '../../components/sidebar'
|
||||
import EditPage from '../../components/edit-page'
|
||||
|
||||
interface RootRouteProps {
|
||||
@@ -10,14 +9,11 @@ interface RootRouteProps {
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Sidebar />
|
||||
<AppShell.Main>
|
||||
<Container component="article" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</>
|
||||
<AppShell.Main>
|
||||
<Container component="article" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ import { Head } from 'tuono'
|
||||
<title>Tuono - Contributing</title>
|
||||
</Head>
|
||||
|
||||
# How to contribute
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="✨ Contributing" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Contributing
|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -12,15 +18,31 @@ The project is massive - if you like it do consider to contribute!
|
||||
|
||||
## Getting started
|
||||
|
||||
Currently I'm keeping a private dashboard to prioritize new features and bug fixes.
|
||||
The `tuono` project can mostly be split by the following sub-domains:
|
||||
|
||||
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)
|
||||
- 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
|
||||
|
||||
To contribute is not strictly required to know both typescript and rust (even though it
|
||||
would be a great nice to have). Most of the feature tasks and bugfixes can be handled solely on
|
||||
the client or on the server side.
|
||||
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
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Head } from 'tuono'
|
||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Installation</title>
|
||||
</Head>
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Installation" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -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
|
||||
@@ -4,6 +4,13 @@ import { Head } from 'tuono'
|
||||
<title>Tuono - Routing</title>
|
||||
</Head>
|
||||
|
||||
# Routing
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
<Breadcrumbs>
|
||||
<Element label="Routing" href="/documentation/routing" />
|
||||
<Element label="Project structure" />
|
||||
</Breadcrumbs>
|
||||
|
||||
# Project structure
|
||||
|
||||
Todo
|
||||
|
||||
@@ -4,6 +4,14 @@ import { Head } from 'tuono'
|
||||
<title>Tuono - Tutorial intro</title>
|
||||
</Head>
|
||||
|
||||
# Tutorial intro
|
||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
||||
|
||||
TODO
|
||||
<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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.8.4"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.8.4"
|
||||
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.4"}
|
||||
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.4"
|
||||
version = "0.9.2"
|
||||
edition = "2021"
|
||||
description = "The react/rust fullstack framework"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-fs-router-vite-plugin",
|
||||
"version": "0.8.4",
|
||||
"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.4",
|
||||
"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";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono-router",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.2",
|
||||
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
@@ -42,6 +42,7 @@
|
||||
"react-dom": ">=16.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-intersection-observer": "^9.13.0",
|
||||
"vite": "^5.2.11",
|
||||
"zustand": "4.4.7"
|
||||
},
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
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(
|
||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
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} onClick={handleTransition}>
|
||||
<a {...props} ref={ref} onClick={handleTransition}>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest'
|
||||
import { getRouteByPathname } from './Matches'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
|
||||
describe('Test getRouteByPathname fn', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
test('match routes by ids', () => {
|
||||
vi.mock('../hooks/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(getRouteByPathname('/')?.id).toBe('/')
|
||||
expect(getRouteByPathname('/not-found')?.id).toBe(undefined)
|
||||
expect(getRouteByPathname('/about')?.id).toBe('/about')
|
||||
expect(getRouteByPathname('/posts/')?.id).toBe('/posts/')
|
||||
expect(getRouteByPathname('/posts/dynamic-post')?.id).toBe('/posts/[post]')
|
||||
expect(getRouteByPathname('/posts/defined-post')?.id).toBe(
|
||||
'/posts/defined-post',
|
||||
)
|
||||
expect(getRouteByPathname('/posts/dynamic-post/dynamic-comment')?.id).toBe(
|
||||
'/posts/[post]/[comment]',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,71 +1,18 @@
|
||||
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'
|
||||
import useRoute from '../hooks/useRoute'
|
||||
|
||||
interface MatchesProps {
|
||||
// user defined props
|
||||
serverSideProps: any
|
||||
}
|
||||
|
||||
const DYNAMIC_PATH_REGEX = /\[(.*?)\]/
|
||||
|
||||
/*
|
||||
* This function 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 {
|
||||
const { routesById } = useInternalRouter()
|
||||
|
||||
if (routesById[pathname]) return routesById[pathname]
|
||||
|
||||
const dynamicRoutes = Object.keys(routesById).filter((route) =>
|
||||
DYNAMIC_PATH_REGEX.test(route),
|
||||
)
|
||||
|
||||
if (!dynamicRoutes.length) return
|
||||
|
||||
const pathSegments = pathname.split('/').filter(Boolean)
|
||||
|
||||
let match = undefined
|
||||
|
||||
// TODO: Check algo efficiency
|
||||
for (const dynamicRoute of dynamicRoutes) {
|
||||
const dynamicRouteSegments = dynamicRoute.split('/').filter(Boolean)
|
||||
|
||||
const routeSegmentsCollector: string[] = []
|
||||
|
||||
for (let i = 0; i < dynamicRouteSegments.length; i++) {
|
||||
if (
|
||||
dynamicRouteSegments[i] === pathSegments[i] ||
|
||||
DYNAMIC_PATH_REGEX.test(dynamicRouteSegments[i] || '')
|
||||
) {
|
||||
routeSegmentsCollector.push(dynamicRouteSegments[i] ?? '')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (routeSegmentsCollector.length === pathSegments.length) {
|
||||
match = `/${routeSegmentsCollector.join('/')}`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) return
|
||||
return routesById[match]
|
||||
}
|
||||
|
||||
export function Matches({ serverSideProps }: MatchesProps): JSX.Element {
|
||||
const location = useRouterStore((st) => st.location)
|
||||
|
||||
const route = getRouteByPathname(location.pathname)
|
||||
const route = useRoute(location.pathname)
|
||||
|
||||
if (!route) {
|
||||
return <NotFound />
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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]',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Route } from '../route'
|
||||
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 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 default function useRoute(pathname?: string): Route | undefined {
|
||||
if (!pathname) return
|
||||
|
||||
pathname = sanitizePathname(pathname)
|
||||
|
||||
const { routesById } = useInternalRouter()
|
||||
|
||||
if (routesById[pathname]) return routesById[pathname]
|
||||
|
||||
const dynamicRoutes = Object.keys(routesById).filter((route) =>
|
||||
DYNAMIC_PATH_REGEX.test(route),
|
||||
)
|
||||
|
||||
if (!dynamicRoutes.length) return
|
||||
|
||||
const pathSegments = pathname.split('/').filter(Boolean)
|
||||
|
||||
let match = undefined
|
||||
|
||||
// TODO: Check algo efficiency
|
||||
for (const dynamicRoute of dynamicRoutes) {
|
||||
const dynamicRouteSegments = dynamicRoute.split('/').filter(Boolean)
|
||||
|
||||
const routeSegmentsCollector: string[] = []
|
||||
|
||||
for (let i = 0; i < dynamicRouteSegments.length; i++) {
|
||||
if (
|
||||
dynamicRouteSegments[i] === pathSegments[i] ||
|
||||
DYNAMIC_PATH_REGEX.test(dynamicRouteSegments[i] || '')
|
||||
) {
|
||||
routeSegmentsCollector.push(dynamicRouteSegments[i] ?? '')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (routeSegmentsCollector.length === pathSegments.length) {
|
||||
match = `/${routeSegmentsCollector.join('/')}`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!match) return
|
||||
return routesById[match]
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -6,7 +6,7 @@ interface RouteOptions {
|
||||
isRoot?: boolean
|
||||
getParentRoute?: () => Route
|
||||
path?: string
|
||||
component: () => JSX.Element
|
||||
component: RouteComponent
|
||||
}
|
||||
|
||||
export function createRoute(options: RouteOptions): Route {
|
||||
|
||||
@@ -17,4 +17,6 @@ export interface RouteProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export type RouteComponent = (props: RouteProps) => JSX.Element
|
||||
export type RouteComponent = ((props: RouteProps) => JSX.Element) & {
|
||||
preload: () => void
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tuono",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.2",
|
||||
"description": "The react/rust fullstack framework",
|
||||
"scripts": {
|
||||
"dev": "vite build --watch",
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import Head from 'react-meta-tags'
|
||||
import {
|
||||
createRoute,
|
||||
createRootRoute,
|
||||
createRouter,
|
||||
Link,
|
||||
RouterProvider,
|
||||
dynamic,
|
||||
useRouter,
|
||||
} from 'tuono-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'
|
||||
|
||||
Reference in New Issue
Block a user