Compare commits

...

22 Commits

Author SHA1 Message Date
Valerio Ageno f1ab7df5ce feat: udpdate version to v0.16.6 2024-12-22 18:06:40 +01:00
Marco Pasqualetti 986de49052 refactor: add eslint-plugin-react-hooks (#248) 2024-12-22 18:03:03 +01:00
Marco Pasqualetti d09ca5548a chore: add .editorconfig (#249) 2024-12-22 17:52:03 +01:00
Marco Pasqualetti 77e22c40b6 refactor: enable typescript-eslint strict rules (#243) 2024-12-22 12:08:21 +01:00
Marco Pasqualetti de74647c43 test(packages/tuono): messageChannel - use vi.fn (#241) 2024-12-22 09:30:18 +01:00
Mustafa Zaki Assagaf 498601c21a feat: add MessageChannel polyfill to support react 19 (#238) 2024-12-21 16:55:04 +01:00
Marco Pasqualetti e99f06ddf5 chore(bug_report): make system-info fields required and add CLI command to retrieve them (#239) 2024-12-20 07:34:55 +01:00
Marco Pasqualetti f06af59168 chore: add CODEOWNERS (#237) 2024-12-19 08:22:16 +01:00
Valerio Ageno d50e3a7fc0 feat: update version to v0.16.5 2024-12-18 22:10:05 +01:00
Valerio Ageno 3e4e7ff1ab feat: update rendering from renderToString to renderToReadableStream (#233)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-18 21:58:19 +01:00
Marco Pasqualetti 21ff3c8655 docs: update tuono (#236) 2024-12-18 21:53:24 +01:00
Valerio Ageno 166e61bab4 feat: update version to v0.16.4 2024-12-18 14:14:51 +01:00
Mustafa Zaki Assagaf 93cd8bd8da fix: catch-all paths replace double underscore with single underscore (#229) 2024-12-18 14:06:14 +01:00
Marco Pasqualetti f5c1641af2 chore(apps/documentation): remove tsconfig.node.json (#228) 2024-12-18 09:54:52 +01:00
Mustafa Zaki Assagaf 62ffed0624 docs: added website link to creates documentation (#230) 2024-12-18 09:13:03 +01:00
Marco Pasqualetti 8d0a4a7417 refactor(packages/lazy-fn-vite-plugin): add filter to avoid processing unrelated files (#227) 2024-12-18 09:11:30 +01:00
Mustafa Zaki Assagaf b1b274ea12 feat: update tuono macro handler naming to avoid naming collisions (#225) 2024-12-17 16:47:23 +01:00
Valerio Ageno fc38865965 fix: README.md 2024-12-16 13:57:25 +01:00
Valerio Ageno 911adaaecb feat: update version to v0.16.3 2024-12-16 08:08:27 +01:00
Marco Pasqualetti 7626ed8d1d fix(packages/lazy-fn-vite-plugin): make dynamic function detection stricter (#223) 2024-12-16 08:03:37 +01:00
Valerio Ageno 6135fd7e1b docs: add dynamic routes page (#219)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2024-12-15 13:39:06 +01:00
Valerio Ageno 14bea36356 fix: update lazyLoadComponent fn name to __tuono__internal__lazyLoadComponent (#222) 2024-12-15 09:22:19 +01:00
74 changed files with 884 additions and 593 deletions
+7
View File
@@ -0,0 +1,7 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = space
indent_size = 2
max_line_length = 80
+14 -45
View File
@@ -32,55 +32,24 @@ body:
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: input
id: tuono-version
- type: textarea
id: system-info
attributes:
label: Tuono version
placeholder: '[e.g. 0.4.0]'
label: System Info
description: Output of `npx envinfo --system --npmPackages 'tuono' --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
validations:
required: false
- type: input
id: os-version
required: true
- type: textarea
id: system-info-rust
attributes:
label: OS
placeholder: '[e.g. MacOS, Windows]'
label: System info (Rust)
description: Output of `rustc --version && cargo --version && tuono --version`
render: shell
placeholder: 'rustc, cargo and tuono binary version'
validations:
required: false
- type: input
id: browser-version
attributes:
label: Browser
placeholder: '[e.g. chrome, safari]'
validations:
required: false
- type: input
id: node-version
attributes:
label: Node version
placeholder: '[e.g. 20.0.0]'
validations:
required: false
- type: input
id: rust-version
attributes:
label: Rust version
placeholder: '[e.g. 1.79.0]'
validations:
required: false
- type: input
id: create-version
attributes:
label: Crate version
placeholder: '[e.g. 1.78.0]'
validations:
required: false
- type: input
id: node-package-manager-version
attributes:
label: Node Package Manger version
placeholder: '[e.g. pnpm: 9.5.0] '
validations:
required: false
required: true
- type: textarea
id: additional-context
attributes:
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.15.0
run: cargo install tuono@0.16.4
- name: Build project
working-directory: ./apps/documentation
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install tuono
run: cargo install tuono@0.16.0
run: cargo install tuono@0.16.4
- name: Build project
working-directory: ./apps/documentation
+2
View File
@@ -8,6 +8,8 @@ dist/
examples/*/pnpm-lock.yaml
vite.config.ts.timestamp*
## Rust related ignores
# Generated by Cargo
+6 -1
View File
@@ -1 +1,6 @@
pnpm-lock.yaml
pnpm-lock.yaml
dist
.tuono
packages/lazy-fn-vite-plugin/tests/sources/*
+9
View File
@@ -0,0 +1,9 @@
* @Valerioageno @marcalexiei
# Rust
/crates/ @Valerioageno
/Cargo.toml @Valerioageno
# Misc
/.github/ @marcalexiei
+1 -1
View File
@@ -26,7 +26,7 @@ Some of its features are:
## 📖 Documentation
The [documentation](https://tuono.dev/documentation) is available on
The [documentation](https://tuono.dev/) is available on
[tuono.dev](https://tuono.dev/).
## Introduction
+1 -1
View File
@@ -8,7 +8,7 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = "0.15.0"
tuono_lib = "0.16.4"
glob = "0.3.1"
time = { version = "0.3", features = ["macros"] }
serde = { version = "1.0.202", features = ["derive"] }
+2 -2
View File
@@ -6,7 +6,7 @@
"type": "module",
"scripts": {
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown .",
"format": "prettier --write --ignore-unknown .",
"format:check": "prettier --check --ignore-unknown .",
"types": "tsc --noEmit"
},
@@ -19,7 +19,7 @@
"clsx": "2.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"tuono": "npm:tuono@0.16.0"
"tuono": "npm:tuono@0.16.4"
},
"devDependencies": {
"@mdx-js/rollup": "3.1.0",
@@ -111,6 +111,11 @@ export const sidebarElements: Array<SidebarElement> = [
label: 'Defining routes',
href: '/documentation/routing/defining-routes',
},
{
type: 'element',
label: 'Dynamic routes',
href: '/documentation/routing/dynamic-routes',
},
{
type: 'element',
label: 'Link and navigation',
@@ -126,11 +131,6 @@ export const sidebarElements: Array<SidebarElement> = [
label: 'Loading state',
href: '/documentation/routing/loading-state',
},
{
type: 'element',
label: 'Dynamic routes',
href: '/documentation/routing/dynamic-routes',
},
{
type: 'element',
label: 'Layouts',
@@ -64,7 +64,9 @@ export function TableOfContents({
),
)
window.addEventListener('scroll', handleScroll)
return (): void => window.removeEventListener('scroll', handleScroll)
return (): void => {
window.removeEventListener('scroll', handleScroll)
}
}, [router.pathname])
if (filteredHeadings.length === 0) {
@@ -17,9 +17,9 @@ export default function ThemeBtn(): JSX.Element {
return (
<ActionIcon
onClick={() =>
onClick={() => {
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
}
}}
variant="default"
size="lg"
aria-label="Toggle color scheme"
@@ -9,6 +9,53 @@ import Breadcrumbs, { Element } from '@/components/breadcrumbs'
<Breadcrumbs breadcrumbs={[{ label: 'Dynamic routes' }]} />
# Loading state
# Dynamic routes
Todo
> 🚧 Only available for SSR apps. This feature will be soon available also for static generated apps.
When the exact segment names are not known in advance and you need to generate routes from dynamic data,
you can use dynamic segments, which are populated at request time.
## Convention
A dynamic segment can be created by wrapping a
file or a folder name in square brackets: `[segmentName]`.
The dynamic segment can be used in folders as well as file names:
- `src/routes/blog/[slug].tsx`
- `src/routes/blog/[slug]/index.tsx`
A path can contain multiple dynamic segments, allowing for more flexible routing:
- `src/routes/blog/[post]/[comment].tsx`
### Example
Assuming you're working on a blog with multiple posts,
you can use a dynamic segment to define the post page: `src/routes/blog/[slug].tsx`.
The `[slug]` is the dynamic segment which will be used to identify individual blog posts.
```tsx
import { useRouter } from 'tuono'
export default function Page() {
const router = useRouter()
return <p>Post: {router.pathname}</p>
}
```
## Catch all segments
Dynamic Segments can be extended to catch-all subsequent segments by adding
an ellipsis inside the brackets `[...segmentName]`.
Given the following `src/routes/shop/[...slug].tsx`,
the following paths will be matched:
- `/shop/clothes`
- `/shop/clothes/tops`
- `/shop`
- `/shop/clothes/tops/t-shirts`
- and so on.
+1 -2
View File
@@ -32,6 +32,5 @@
// Completeness
"skipLibCheck": true
},
"include": ["src", "tuono.config.ts", "postcss.config.js"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src", "tuono.config.ts", "postcss.config.js"]
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
-3
View File
@@ -7,9 +7,6 @@ const config: TuonoConfig = {
'@': 'src',
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
},
optimizeDeps: {
exclude: ['@mdx-js/react'],
},
plugins: [
{ enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react' }) },
],
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono"
version = "0.16.2"
version = "0.16.6"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
+5
View File
@@ -1,3 +1,8 @@
//! ## Tuono
//! Tuono is a full-stack web framework for building React applications using Rust as the backend with a strong focus on usability and performance.
//!
//! You can find the full documentation at [tuono.dev](https://tuono.dev/)
mod app;
pub mod cli;
mod mode;
+1 -1
View File
@@ -52,7 +52,7 @@ impl AxumInfo {
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace("...", "_catch_all_")
.replace("...", "catch_all_")
.replace(']', ""),
axum_route: axum_route
.replace("[...", "*")
+2 -2
View File
@@ -84,11 +84,11 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
r#".route("{axum_route}", get({module_import}::tuono__internal__route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::api))"#
r#".route("/__tuono/data{axum_route}", get({module_import}::tuono__internal__api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
+13 -8
View File
@@ -30,7 +30,7 @@ fn it_successfully_create_the_index_route() {
assert!(temp_main_rs_content.contains("mod index;"));
assert!(temp_main_rs_content
.contains(r#".route("/", get(index::route)).route("/__tuono/data/", get(index::api))"#));
.contains(r#".route("/", get(index::tuono__internal__route)).route("/__tuono/data/", get(index::tuono__internal__api))"#));
}
#[test]
@@ -120,18 +120,23 @@ fn it_successfully_create_catch_all_routes() {
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/[...all_apis].rs"]"#));
assert!(temp_main_rs_content.contains("mod api_dyn__catch_all_all_apis;"));
assert!(temp_main_rs_content.contains("mod api_dyn_catch_all_all_apis;"));
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/[...all_routes].rs"]"#));
assert!(temp_main_rs_content.contains("mod dyn__catch_all_all_routes;"));
assert!(temp_main_rs_content.contains("mod dyn_catch_all_all_routes;"));
assert!(temp_main_rs_content.contains(
r#".route("/api/*all_apis", post(api_dyn__catch_all_all_apis::post__tuono_internal_api))"#
r#".route("/api/*all_apis", post(api_dyn_catch_all_all_apis::post__tuono_internal_api))"#
));
assert!(temp_main_rs_content.contains(
r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__route))"#
));
assert!(temp_main_rs_content.contains(
r#".route("/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__route))"#
));
assert!(temp_main_rs_content
.contains(r#".route("/*all_routes", get(dyn__catch_all_all_routes::route))"#));
assert!(temp_main_rs_content
.contains(r#".route("/__tuono/data/*all_routes", get(dyn__catch_all_all_routes::api))"#));
.contains(r#".route("/__tuono/data/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__api))"#));
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib"
version = "0.16.2"
version = "0.16.6"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
@@ -32,7 +32,7 @@ either = "1.13.0"
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.16.2"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.16.6"}
# Match the same version used by axum
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
+5
View File
@@ -1,3 +1,8 @@
//! ## Tuono
//! Tuono is a full-stack web framework for building React applications using Rust as the backend with a strong focus on usability and performance.
//!
//! You can find the full documentation at [tuono.dev](https://tuono.dev/)
mod catch_all;
mod logger;
mod manifest;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tuono_lib_macros"
version = "0.16.2"
version = "0.16.6"
edition = "2021"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
+2 -2
View File
@@ -45,7 +45,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
#item
pub async fn route(
pub async fn tuono__internal__route(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
@@ -59,7 +59,7 @@ pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
}
pub async fn api(
pub async fn tuono__internal__api(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
+5
View File
@@ -1,3 +1,8 @@
//! ## Tuono
//! Tuono is a full-stack web framework for building React applications using Rust as the backend with a strong focus on usability and performance.
//!
//! You can find the full documentation at [tuono.dev](https://tuono.dev/)
extern crate proc_macro;
use proc_macro::TokenStream;
+38 -12
View File
@@ -2,6 +2,8 @@ import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
// @ts-expect-error no types are available for this plugin
import eslintPluginImport from 'eslint-plugin-import'
// @ts-expect-error no types are available for this plugin
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
export default tseslint.config(
{
@@ -14,6 +16,7 @@ export default tseslint.config(
// #region package-specific
'packages/fs-router-vite-plugin/tests/generator/**',
'packages/lazy-fn-vite-plugin/tests/sources/**',
'packages/tuono/bin/**',
// #endregion package-specific
@@ -24,10 +27,17 @@ export default tseslint.config(
reportUnusedDisableDirectives: 'error',
},
},
eslint.configs.recommended,
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
eslintPluginImport.flatConfigs.recommended,
eslintPluginImport.flatConfigs.typescript,
tseslint.configs.recommendedTypeChecked,
/* eslint-enable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
// eslint-disable-next-line import/no-named-as-default-member
tseslint.configs.strictTypeChecked,
{
languageOptions: {
parserOptions: {
@@ -49,16 +59,13 @@ export default tseslint.config(
rules: {
// #region @typescript-eslint
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
'@typescript-eslint/no-wrapper-object-types': 'error',
'@typescript-eslint/no-empty-object-type': 'error',
'@typescript-eslint/no-unsafe-function-type': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-definitions': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ prefer: 'type-imports' },
],
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/method-signature-style': ['error', 'property'],
'@typescript-eslint/naming-convention': [
'error',
@@ -73,19 +80,24 @@ export default tseslint.config(
},
},
],
'@typescript-eslint/no-deprecated': 'error',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unnecessary-condition': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-inferrable-types': [
'error',
{ ignoreParameters: true },
],
'@typescript-eslint/restrict-template-expressions': [
'error',
{
allowAny: false,
allowBoolean: false,
allowNever: false,
allowNullish: false,
allowNumber: true,
allowRegExp: false,
},
],
// #endregion @typescript-eslint
// #region import
@@ -126,8 +138,22 @@ export default tseslint.config(
'no-shadow': 'error',
'no-undef': 'off',
'sort-imports': 'off',
// #endregion misc
},
},
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
plugins: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
'react-hooks': eslintPluginReactHooks,
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
},
},
{
files: ['apps/documentation/**'],
settings: {
+1 -2
View File
@@ -31,8 +31,7 @@
"eslint": "^9.15.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-hooks": "5.1.0",
"prettier": "^3.2.4",
"turbo": "^2.2.3",
"typescript": "5.6.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.16.2",
"version": "0.16.6",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -3,7 +3,7 @@ import type { RouteNode } from './types'
export function buildRouteConfig(nodes: Array<RouteNode>, depth = 1): string {
const children = nodes.map((node) => {
const route = `${node.variableName}Route`
const route = `${node.variableName as string}Route`
if (node.children?.length) {
const childConfigs = buildRouteConfig(node.children, depth + 1)
@@ -157,14 +157,14 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
(d): number => (d.routePath.includes(`/${ROOT_PATH_ID}`) ? -1 : 1),
(d): number => d.routePath.split('/').length,
(d): number => (d.routePath.endsWith("index'") ? -1 : 1),
(d): any => d,
(d): RouteNode => d,
])
const imports = [
...sortedRouteNodes.map((node) => {
const extension = node.filePath.endsWith('mdx') ? '.mdx' : ''
return `const ${
node.variableName
node.variableName as string
}Import = dynamic(() => import('./${replaceBackslash(
removeExt(
path.relative(
@@ -181,18 +181,21 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
...sortedRouteNodes.map((node) => {
const isRoot = node.routePath.endsWith(ROOT_PATH_ID)
const rootDeclaration = isRoot ? ', isRoot: true' : ''
const variableName = node.variableName as string
return `const ${node.variableName} = createRoute({ component: ${node.variableName}Import${rootDeclaration} })`
return `const ${variableName} = createRoute({ component: ${variableName}Import${rootDeclaration} })`
}),
].join('\n')
const createRouteUpdates = [
sortedRouteNodes
.map((node) => {
const variableName = node.variableName as string
const cleanedPath = node.cleanedPath as string
return [
`const ${node.variableName}Route = ${node.variableName}.update({
`const ${variableName}Route = ${variableName}.update({
${[
!node.path?.endsWith(ROOT_PATH_ID) && `path: '${node.cleanedPath}'`,
!node.path?.endsWith(ROOT_PATH_ID) && `path: '${cleanedPath}'`,
`getParentRoute: () => ${node.parent?.variableName ?? 'root'}Route`,
rustHandlersNodes.includes(node.path || '')
? 'hasHandler: true'
@@ -236,7 +239,7 @@ export async function routeGenerator(config = defaultConfig): Promise<void> {
const routeTreeContent = await fsp
.readFile(path.resolve(config.generatedRouteTree), 'utf-8')
.catch((e) => {
.catch((e: unknown) => {
const err = e as Error & { code?: string }
if (err.code === 'ENOENT') {
return undefined
@@ -5,7 +5,7 @@ import { describe, it, expect } from 'vitest'
import { routeGenerator } from '../src/generator'
describe('generator works', async () => {
const folderNames = await fs.readdir(process.cwd() + '/tests/generator')
const folderNames = await fs.readdir(`${process.cwd()}/tests/generator`)
it.each(folderNames)(
'should wire-up the routes for a "%s" tree',
@@ -1,2 +0,0 @@
dist
pnpm-lock.yaml
+3 -3
View File
@@ -1,14 +1,14 @@
{
"name": "tuono-lazy-fn-vite-plugin",
"version": "0.16.2",
"version": "0.16.6",
"description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
"types": "tsc --noEmit",
"test:watch": "vitest",
"test": "vitest run"
@@ -1,3 +1,3 @@
export const TUONO_DYNAMIC_FN_ID = 'dynamic'
export const TUONO_LAZY_FN_ID = 'lazyLoadComponent'
export const TUONO_LAZY_FN_ID = '__tuono__internal__lazyLoadComponent'
export const TUONO_MAIN_PACKAGE = 'tuono'
+95 -45
View File
@@ -1,13 +1,8 @@
import * as babel from '@babel/core'
import * as t from '@babel/types'
import type { Plugin } from 'vite'
import type { PluginItem } from '@babel/core'
import type {
Identifier,
CallExpression,
ArrowFunctionExpression,
StringLiteral,
} from '@babel/types'
import { transformSync } from '@babel/core'
import type { PluginItem as BabelPluginItem } from '@babel/core'
import * as BabelTypes from '@babel/types'
import { createFilter } from 'vite'
import type { Plugin as VitePlugin, Rollup, FilterPattern } from 'vite'
import {
TUONO_MAIN_PACKAGE,
@@ -20,11 +15,24 @@ import { isTuonoDynamicFnImported } from './utils'
* [SERVER build]
* This plugin just removes the `dynamic` imported function from any tuono import
*/
const RemoveTuonoLazyImport: PluginItem = {
const RemoveTuonoLazyImport: BabelPluginItem = {
name: 'remove-tuono-lazy-import-plugin',
visitor: {
ImportSpecifier: (path) => {
if (isTuonoDynamicFnImported(path)) {
ImportDeclaration: (path) => {
const importNode = path.node
if (importNode.source.value !== TUONO_MAIN_PACKAGE) return
path.traverse({
ImportSpecifier: (importSpecifierPath) => {
if (isTuonoDynamicFnImported(importSpecifierPath)) {
importSpecifierPath.remove()
}
},
})
// If there are no specifiers left after traverse
// remove the import to avoid unwanted side effects
if (importNode.specifiers.length === 0) {
path.remove()
}
},
@@ -33,36 +41,56 @@ const RemoveTuonoLazyImport: PluginItem = {
/**
* [CLIENT build]
* This plugin replace the `dynamic` function with the `lazyLoadComponent` one
* This plugin replace the `dynamic` function with the `__tuono__internal__lazyLoadComponent` one
*/
const ReplaceTuonoLazyImport: PluginItem = {
name: 'remove-tuono-lazy-import-plugin',
const ReplaceTuonoLazyImport: BabelPluginItem = {
name: 'replace-tuono-lazy-import-plugin',
visitor: {
ImportSpecifier: (path) => {
if (isTuonoDynamicFnImported(path)) {
;(path.node.imported as Identifier).name = TUONO_LAZY_FN_ID
if (
BabelTypes.isIdentifier(path.node.imported) &&
isTuonoDynamicFnImported(path)
) {
path.node.imported.name = TUONO_LAZY_FN_ID
}
},
},
}
const turnLazyIntoStatic = {
VariableDeclaration: (path: babel.NodePath<t.VariableDeclaration>): void => {
path.node.declarations.forEach((el) => {
const init = el.init as CallExpression
if ((init.callee as Identifier).name === TUONO_DYNAMIC_FN_ID) {
const importName = (el.id as Identifier).name
const importPath = (
(
(init.arguments[0] as ArrowFunctionExpression)
.body as CallExpression
).arguments[0] as StringLiteral
).value
VariableDeclaration: (
path: babel.NodePath<BabelTypes.VariableDeclaration>,
): void => {
path.node.declarations.forEach((variableDeclarationNode) => {
const init = variableDeclarationNode.init
if (
BabelTypes.isCallExpression(init) &&
// ensures that the method call is `TUONO_DYNAMIC_FN_ID`
BabelTypes.isIdentifier(init.callee, { name: TUONO_DYNAMIC_FN_ID }) &&
// import name must be an identifier
BabelTypes.isIdentifier(variableDeclarationNode.id) &&
// check that the first function parameter is an arrow function
BabelTypes.isArrowFunctionExpression(init.arguments[0])
) {
const cmpImportFn = init.arguments[0]
// ensures that the first parameter is a call expression (may be a block statement)
if (!BabelTypes.isCallExpression(cmpImportFn.body)) return
// ensures that the first parameter is a string literal (the import path)
if (!BabelTypes.isStringLiteral(cmpImportFn.body.arguments[0])) return
const importName = variableDeclarationNode.id.name
const importPath = cmpImportFn.body.arguments[0].value
if (importName && importPath) {
const importDeclaration = t.importDeclaration(
[t.importDefaultSpecifier(t.identifier(importName))],
t.stringLiteral(importPath),
const importDeclaration = BabelTypes.importDeclaration(
[
BabelTypes.importDefaultSpecifier(
BabelTypes.identifier(importName),
),
],
BabelTypes.stringLiteral(importPath),
)
path.replaceWith(importDeclaration)
@@ -76,7 +104,7 @@ const turnLazyIntoStatic = {
* [SERVER build]
* This plugin statically imports the lazy loaded components
*/
const TurnLazyIntoStaticImport: PluginItem = {
const TurnLazyIntoStaticImport: BabelPluginItem = {
name: 'turn-lazy-into-static-import-plugin',
visitor: {
Program: (path) => {
@@ -91,28 +119,50 @@ const TurnLazyIntoStaticImport: PluginItem = {
},
}
export function LazyLoadingPlugin(): Plugin {
interface LazyLoadingPluginOptions {
include: FilterPattern
}
export function LazyLoadingPlugin(
options: LazyLoadingPluginOptions,
): VitePlugin {
const { include } = options
const filter = createFilter(include)
return {
name: 'vite-plugin-tuono-lazy-loading',
enforce: 'pre',
transform(code, _id, opts): string | undefined | null {
transform(code, id, opts): Rollup.TransformResult {
if (!filter(id)) return
/**
* @todo we should exclude non tsx files from this transformation
* this might benefit build time avoiding running `includes` on non-tsx files.
* This can be executed using `_id` parameter
* which is the filepath that is being processed
*/
if (
code.includes(TUONO_DYNAMIC_FN_ID) &&
code.includes(TUONO_MAIN_PACKAGE)
) {
const res = babel.transformSync(code, {
plugins: [
['@babel/plugin-syntax-jsx', {}],
['@babel/plugin-syntax-typescript', { isTSX: true }],
[!opts?.ssr ? ReplaceTuonoLazyImport : []],
[opts?.ssr ? RemoveTuonoLazyImport : []],
[opts?.ssr ? TurnLazyIntoStaticImport : []],
],
sourceMaps: true,
})
const plugins: Array<BabelPluginItem> = [
['@babel/plugin-syntax-jsx', {}],
['@babel/plugin-syntax-typescript', { isTSX: true }],
]
if (opts?.ssr) {
plugins.push(RemoveTuonoLazyImport, TurnLazyIntoStaticImport)
} else {
plugins.push(ReplaceTuonoLazyImport)
}
const res = transformSync(code, { plugins })
return res?.code
}
return code
},
}
+20 -17
View File
@@ -1,22 +1,25 @@
import type {
Identifier,
ImportDeclaration,
ImportSpecifier,
import {
isIdentifier,
isImportDeclaration,
isImportSpecifier,
} from '@babel/types'
import { TUONO_MAIN_PACKAGE, TUONO_DYNAMIC_FN_ID } from './constants'
export const isTuonoDynamicFnImported = (
path: babel.NodePath<ImportSpecifier>,
): boolean => {
if ((path.node.imported as Identifier).name !== TUONO_DYNAMIC_FN_ID) {
return false
}
if (
(path.parentPath.node as ImportDeclaration).source.value !==
TUONO_MAIN_PACKAGE
) {
return false
}
return true
/**
* By a given AST Node path returns true if the path involves an import specifier
* importing {@link TUONO_DYNAMIC_FN_ID}
*/
export const isTuonoDynamicFnImported = (path: babel.NodePath): boolean => {
// If the node isn't an import declaration there is no need to process it
if (!isImportDeclaration(path.parentPath?.node)) return false
// if the import doesn't import from 'tuono' we don't need to process it
if (path.parentPath.node.source.value !== TUONO_MAIN_PACKAGE) return false
// ensure that we are processing an import specifier
if (!isImportSpecifier(path.node)) return false
// finally check if the imported item is `TUONO_DYNAMIC_FN_ID`
return isIdentifier(path.node.imported, { name: TUONO_DYNAMIC_FN_ID })
}
@@ -0,0 +1,9 @@
import React, { Suspense, type JSX } from "react";
import { __tuono__internal__lazyLoadComponent as dynamic } from "tuono";
const DynamicComponent = dynamic(() => import("../components/DynamicComponent"));
const Loading = (): JSX.Element => <>Loading</>;
export default function IndexPage(): JSX.Element {
return <Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>;
}
@@ -0,0 +1,8 @@
import React, { Suspense, type JSX } from "react";
import DynamicComponent from "../components/DynamicComponent";
const Loading = (): JSX.Element => <>Loading</>;
export default function IndexPage(): JSX.Element {
return <Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>;
}
@@ -0,0 +1,14 @@
import React, { Suspense, type JSX } from "react";
import { dynamic } from "tuono";
const DynamicComponent = dynamic(() => import("../components/DynamicComponent"))
const Loading = (): JSX.Element => <>Loading</>
export default function IndexPage(): JSX.Element {
return (
<Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>
);
}
@@ -0,0 +1,4 @@
import { createRoute } from 'tuono';
import { dynamic } from 'external-lib';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
@@ -0,0 +1,4 @@
import { createRoute } from 'tuono';
import { dynamic } from 'external-lib';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
@@ -0,0 +1,7 @@
import { createRoute } from 'tuono'
import { dynamic } from 'external-lib'
const IndexImport = dynamic(() => import('./../src/routes/index'))
const PokemonspokemonImport = dynamic(
() => import('./../src/routes/pokemons/[pokemon]'),
)
@@ -0,0 +1,3 @@
import { createRoute, __tuono__internal__lazyLoadComponent as dynamic } from 'tuono';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));
@@ -0,0 +1,3 @@
import { createRoute } from 'tuono';
import IndexImport from "./../src/routes/index";
import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";
@@ -0,0 +1,6 @@
import { createRoute, dynamic } from 'tuono'
const IndexImport = dynamic(() => import('./../src/routes/index'))
const PokemonspokemonImport = dynamic(
() => import('./../src/routes/pokemons/[pokemon]'),
)
@@ -1,27 +1,11 @@
import fs from 'node:fs/promises'
import os from 'node:os'
import { it, expect, describe } from 'vitest'
import type { Plugin } from 'vite'
import { LazyLoadingPlugin } from '../src'
const SOURCE_CODE = `
import { createRoute, dynamic } from 'tuono'
const IndexImport = dynamic(() => import('./../src/routes/index'))
const PokemonspokemonImport = dynamic(
() => import('./../src/routes/pokemons/[pokemon]'),
)
`
const NON_DYNAMIC_SOURCE = `
import { createRoute } from 'tuono'
import {dynamic} from 'external-lib'
const IndexImport = dynamic(() => import('./../src/routes/index'))
const PokemonspokemonImport = dynamic(
() => import('./../src/routes/pokemons/[pokemon]'),
)
`
type ViteTransformHandler = Exclude<
Plugin['transform'],
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -30,48 +14,81 @@ type ViteTransformHandler = Exclude<
// Create a type-safe transform method
function getTransform(): (...args: Parameters<ViteTransformHandler>) => string {
return LazyLoadingPlugin().transform as never
/** @warning Keep in sync with {@link createBaseViteConfigFromTuonoConfig} */
const pluginFilesInclude = /\.(jsx|js|mdx|md|tsx|ts)$/
return LazyLoadingPlugin({ include: pluginFilesInclude }).transform as never
}
describe('Transpile tuono source', () => {
it('Into the client bundle', () => {
const pluginTransform = getTransform()
const bundle = pluginTransform(SOURCE_CODE, 'id')
expect(bundle)
.toBe(`import { createRoute, lazyLoadComponent as dynamic } from 'tuono';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));`)
})
describe('"dynamic" sources', async () => {
const folderNames = await fs.readdir(`${process.cwd()}/tests/sources`)
it('Into the server bundle', () => {
const pluginTransform = getTransform()
const bundle = pluginTransform(SOURCE_CODE, 'id', {
ssr: true,
describe.each(folderNames)('%s', async (folderName) => {
const testDirPath = `${process.cwd()}/tests/sources/${folderName}`
const sourceFilePath = `${testDirPath}/source.tsx`
const sourceRaw = await fs.readFile(sourceFilePath, 'utf-8')
/**
* When adding `packages/lazy-fn-vite-plugin/tests/sources/dynamic-only` only
* the test involving that fixture were broken on Windows... but not the one in the other fixtures:
* - packages/lazy-fn-vite-plugin/tests/sources/vanilla
* - packages/lazy-fn-vite-plugin/tests/sources/external-dynamic
*
* Awkwardly this doesn't happen on `packages/fs-router-vite-plugin/tests/generator.spec.ts`
*
* Too much pain and sadness to investigate this right now.
* Might worth creating an utility function in the future if this happens again
*/
const source = sourceRaw.replace(new RegExp(os.EOL, 'g'), '\n')
it('should generate file for client', async () => {
const pluginTransform = getTransform()
const clientBundle = pluginTransform(source, sourceFilePath)
const expectedClientSrc = `${testDirPath}/client.expected.tsx`
await expect(clientBundle).toMatchFileSnapshot(
expectedClientSrc,
`${testDirPath} client build should be equal to ${expectedClientSrc}`,
)
})
it('should generate file for server', async () => {
const pluginTransform = getTransform()
const serverBundle = pluginTransform(source, sourceFilePath, {
ssr: true,
})
const expectedServerSrc = `${testDirPath}/server.expected.tsx`
await expect(serverBundle).toMatchFileSnapshot(
expectedServerSrc,
`${testDirPath} server build should be equal to ${expectedServerSrc}`,
)
})
expect(bundle).toBe(`import { createRoute } from 'tuono';
import IndexImport from "./../src/routes/index";
import PokemonspokemonImport from "./../src/routes/pokemons/[pokemon]";`)
})
})
describe('Non tuono dynamic function', () => {
it('Into the client bundle', () => {
const pluginTransform = getTransform()
const bundle = pluginTransform(NON_DYNAMIC_SOURCE, 'id')
expect(bundle).toBe(`import { createRoute } from 'tuono';
import { dynamic } from 'external-lib';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));`)
})
describe('not valid file should not be processed', () => {
const notValidFiles: Array<string> = [
'src/styles/module.css',
'src/pages/file-without-ext',
'src/pages/file-with-invalid-ext',
'src/components/fileWithInvalidExt',
'src/components/sidebar/sidebar-link.module.css',
]
it('Into the server bundle', () => {
it.each(notValidFiles)('"%s"', (fileName) => {
const pluginTransform = getTransform()
const bundle = pluginTransform(NON_DYNAMIC_SOURCE, 'id', {
ssr: true,
})
expect(bundle).toBe(`import { createRoute } from 'tuono';
import { dynamic } from 'external-lib';
const IndexImport = dynamic(() => import('./../src/routes/index'));
const PokemonspokemonImport = dynamic(() => import('./../src/routes/pokemons/[pokemon]'));`)
const code = [
"import { createRoute, dynamic } from 'tuono'",
"const IndexImport = dynamic(() => import('./../src/routes/index'))",
].join('\n')
const result = pluginTransform(code, fileName)
expect(result).toBeUndefined()
})
})
+2 -1
View File
@@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "tests", "vite.config.ts"]
"include": ["src", "tests", "vite.config.ts"],
"exclude": ["tests/sources"]
}
-2
View File
@@ -1,2 +0,0 @@
dist
pnpm-lock.yaml
+3 -3
View File
@@ -1,14 +1,14 @@
{
"name": "tuono-router",
"version": "0.16.2",
"version": "0.16.6",
"description": "React routing component for the framework tuono. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
"types": "tsc --noEmit",
"test:watch": "vitest",
"test": "vitest run"
@@ -42,7 +42,7 @@ describe('Test RouteMatch component', () => {
test('It should correctly render nested routes', () => {
vi.mock('../hooks/useServerSideProps.tsx', () => ({
useServerSideProps: (): { data: any; isLoading: boolean } => {
useServerSideProps: (): { data: unknown; isLoading: boolean } => {
return {
data: undefined,
isLoading: false,
+2 -13
View File
@@ -20,6 +20,7 @@ export const RouteMatch = ({
}: RouteMatchProps): React.JSX.Element => {
const { data, isLoading } = useServerSideProps(route, serverSideProps)
// eslint-disable-next-line react-hooks/exhaustive-deps
const routes = React.useMemo(() => loadParentComponents(route), [route.id])
return (
@@ -29,12 +30,6 @@ export const RouteMatch = ({
)
}
interface ParentProps<TData = unknown> {
children: React.ReactNode
data: TData
isLoading: boolean
}
interface TraverseRootComponentsProps<TData = unknown> {
routes: Array<Route>
data: TData
@@ -58,13 +53,7 @@ const TraverseRootComponents = React.memo(
children,
}: TraverseRootComponentsProps): React.JSX.Element => {
if (routes.length > index) {
const Parent = React.useMemo(
() =>
routes[index]?.component as unknown as (
props: ParentProps,
) => React.JSX.Element,
[],
)
const Parent = (routes[index] as Route).component
return (
<Parent data={data} isLoading={isLoading}>
@@ -2,7 +2,7 @@ import React from 'react'
import type { ReactNode, JSX } from 'react'
import { useListenBrowserUrlUpdates } from '../hooks/useListenBrowserUrlUpdates'
import { initRouterStore } from '../hooks/useRouterStore'
import { useInitRouterStore } from '../hooks/useRouterStore'
import type { ServerProps } from '../types'
import type { Router } from '../router'
@@ -39,7 +39,7 @@ export function RouterProvider({
router,
serverProps,
}: RouterProviderProps): JSX.Element {
initRouterStore(serverProps)
useInitRouterStore(serverProps)
useListenBrowserUrlUpdates()
+9 -5
View File
@@ -1,9 +1,9 @@
import * as React from 'react'
import type { ReactElement, ComponentType } from 'react'
import type { ReactElement } from 'react'
import type { RouteComponent } from './types'
type ImportFn = () => Promise<{ default: React.ComponentType<any> }>
type ImportFn = () => Promise<{ default: RouteComponent }>
/**
* Helper function to lazy load any component.
@@ -41,8 +41,10 @@ export const dynamic = (importFn: ImportFn): React.JSX.Element => {
return <></>
}
export const lazyLoadComponent = (factory: ImportFn): RouteComponent => {
let LoadedComponent: ComponentType<any> | undefined
export const __tuono__internal__lazyLoadComponent = (
factory: ImportFn,
): RouteComponent => {
let LoadedComponent: RouteComponent | undefined
const LazyComponent = React.lazy(factory) as unknown as RouteComponent
const loadComponent = (): Promise<void> =>
@@ -50,7 +52,9 @@ export const lazyLoadComponent = (factory: ImportFn): RouteComponent => {
LoadedComponent = module.default
})
const Component = (props: any): ReactElement =>
const Component = (
props: React.ComponentProps<RouteComponent>,
): ReactElement =>
React.createElement(LoadedComponent || LazyComponent, props)
Component.preload = loadComponent
@@ -9,23 +9,26 @@ import { useRouterStore } from './useRouterStore'
export const useListenBrowserUrlUpdates = (): void => {
const updateLocation = useRouterStore((st) => st.updateLocation)
const updateLocationOnPopStateChange = ({ target }: PopStateEvent): void => {
const { pathname, hash, href, search } = (target as typeof window).location
updateLocation({
pathname,
hash,
href,
searchStr: search,
search: Object.fromEntries(new URLSearchParams(search)),
})
}
useEffect(() => {
const updateLocationOnPopStateChange = ({
target,
}: PopStateEvent): void => {
const { location } = target as typeof window
const { pathname, hash, href, search } = location
updateLocation({
pathname,
hash,
href,
searchStr: search,
search: Object.fromEntries(new URLSearchParams(search)),
})
}
window.addEventListener('popstate', updateLocationOnPopStateChange)
return (): void => {
window.removeEventListener('popstate', updateLocationOnPopStateChange)
}
}, [])
}, [updateLocation])
}
+1 -1
View File
@@ -8,7 +8,7 @@ describe('Test useRoute fn', () => {
test('match routes by ids', () => {
vi.mock('./useInternalRouter.tsx', () => ({
useInternalRouter: (): { routesById: Record<string, any> } => {
useInternalRouter: (): { routesById: Record<string, { id: string }> } => {
return {
routesById: {
'/': { id: '/' },
+2 -2
View File
@@ -25,12 +25,12 @@ export function sanitizePathname(pathname: string): string {
* Optimizations should occour on both
*/
export default function useRoute(pathname?: string): Route | undefined {
const { routesById } = useInternalRouter()
if (!pathname) return
pathname = sanitizePathname(pathname)
const { routesById } = useInternalRouter()
if (routesById[pathname]) return routesById[pathname]
const dynamicRoutes = Object.keys(routesById).filter((route) =>
+1 -1
View File
@@ -16,7 +16,7 @@ interface UseRouterHook {
/**
* This object contains all the query params of the current route
*/
query: Record<string, any>
query: Record<string, unknown>
/**
* Returns the current pathname
+5 -3
View File
@@ -23,7 +23,7 @@ interface RouterState {
updateLocation: (loc: ParsedLocation) => void
}
export const initRouterStore = (props?: ServerProps): void => {
export const useInitRouterStore = (props?: ServerProps): void => {
const updateLocation = useRouterStore((st) => st.updateLocation)
// Init the store in the server in order to correctly
@@ -49,7 +49,7 @@ export const initRouterStore = (props?: ServerProps): void => {
searchStr: search,
search: Object.fromEntries(new URLSearchParams(search)),
})
}, [])
}, [updateLocation])
}
export const useRouterStore = create<RouterState>()((set) => ({
@@ -67,5 +67,7 @@ export const useRouterStore = create<RouterState>()((set) => ({
pendingMatches: [],
cachedMatches: [],
statusCode: 200,
updateLocation: (location: ParsedLocation): void => set({ location }),
updateLocation: (location: ParsedLocation): void => {
set({ location })
},
}))
@@ -13,7 +13,7 @@ interface UseServerSidePropsReturn<TData> {
}
interface TuonoApi {
data?: any
data?: unknown
info: {
redirect_destination?: string
}
@@ -96,7 +96,7 @@ export function useServerSideProps<T>(
return (): void => {
setData(undefined)
}
}, [location.pathname])
}, [location.pathname, route.options.hasHandler, updateLocation])
return { isLoading, data: data as T }
}
+1 -1
View File
@@ -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, lazyLoadComponent } from './dynamic'
export { dynamic, __tuono__internal__lazyLoadComponent } from './dynamic'
export { useRouter } from './hooks/useRouter'
+1 -1
View File
@@ -77,7 +77,7 @@ export class Route {
this.fullPath = fullPath || ''
}
addChildren(routes: Array<Route>): Route {
addChildren(routes: Array<Route>): this {
this.children = routes
return this
}
-2
View File
@@ -17,8 +17,6 @@ interface RouterOptions {
routeTree?: RouteTree
}
export type RouterType = any
export function createRouter(options: CreateRouterOptions): Router {
return new Router(options)
}
+3 -1
View File
@@ -1,4 +1,4 @@
import type { ComponentType as ReactComponentType } from 'react'
import type { ComponentType as ReactComponentType, ReactNode } from 'react'
export interface Segment {
type: 'pathname' | 'param' | 'wildcard'
@@ -17,6 +17,8 @@ export interface ServerProps<TProps = unknown> {
export interface RouteProps<TData = unknown> {
data: TData
isLoading: boolean
children?: ReactNode
}
export type RouteComponent = ReactComponentType<RouteProps> & {
-2
View File
@@ -1,2 +0,0 @@
dist
pnpm-lock.yaml
+6 -4
View File
@@ -1,14 +1,14 @@
{
"name": "tuono",
"version": "0.16.2",
"version": "0.16.6",
"description": "Superfast React fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint .",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"format": "prettier --write --ignore-unknown --ignore-path ../../.prettierignore .",
"format:check": "prettier --check --ignore-unknown --ignore-path ../../.prettierignore .",
"types": "tsc --noEmit",
"test:watch": "vitest",
"test": "vitest run"
@@ -101,6 +101,7 @@
"@babel/template": "^7.24.0",
"@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"@rollup/plugin-inject": "^5.0.5",
"@types/babel__core": "^7.20.5",
"@types/node": "^22.0.0",
"@vitejs/plugin-react-swc": "^3.7.0",
@@ -109,7 +110,8 @@
"tuono-fs-router-vite-plugin": "workspace:*",
"tuono-lazy-fn-vite-plugin": "workspace:*",
"tuono-router": "workspace:*",
"vite": "^5.2.11"
"vite": "^5.2.11",
"web-streams-polyfill": "^4.0.0"
},
"devDependencies": {
"@types/babel-traverse": "^6.25.10",
+26 -3
View File
@@ -1,6 +1,7 @@
import type { InlineConfig } from 'vite'
import type { InlineConfig, Plugin } from 'vite'
import { build, createServer, mergeConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import inject from '@rollup/plugin-inject'
import ViteFsRouter from 'tuono-fs-router-vite-plugin'
import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin'
@@ -9,6 +10,20 @@ import type { TuonoConfig } from '../config'
import { loadConfig, blockingAsync } from './utils'
const VITE_PORT = 3001
const VITE_SSR_PLUGINS: Array<Plugin> = [
{
enforce: 'post',
...inject({
ReadableStream: ['web-streams-polyfill', 'ReadableStream'],
/**
* Added to support `react@19`
* @see https://github.com/tuono-labs/tuono/issues/218
*/
MessageChannel: ['tuono/ssr', 'MessageChannelPolyfill'],
}),
},
]
/**
* From a given {@link TuonoConfig} return a `vite` "mergeable" {@link InlineConfig}
@@ -17,6 +32,12 @@ const VITE_PORT = 3001
function createBaseViteConfigFromTuonoConfig(
tuonoConfig: TuonoConfig,
): InlineConfig {
/**
* @warning Keep in sync with {@link LazyLoadingPlugin} tests:
* packages/lazy-fn-vite-plugin/tests/transpileSource.test.ts
*/
const pluginFilesInclude = /\.(jsx|js|mdx|md|tsx|ts)$/
const viteBaseConfig: InlineConfig = {
root: '.tuono',
logLevel: 'silent',
@@ -41,10 +62,10 @@ function createBaseViteConfigFromTuonoConfig(
* seem broken.
*/
// @ts-expect-error see above comment
react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }),
react({ include: pluginFilesInclude }),
ViteFsRouter(),
LazyLoadingPlugin(),
LazyLoadingPlugin({ include: pluginFilesInclude }),
],
}
@@ -59,6 +80,7 @@ const developmentSSRBundle = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: false,
@@ -137,6 +159,7 @@ const buildProd = (): void => {
mergeConfig<InlineConfig, InlineConfig>(
createBaseViteConfigFromTuonoConfig(config),
{
plugins: VITE_SSR_PLUGINS,
build: {
ssr: true,
minify: true,
+1 -1
View File
@@ -7,8 +7,8 @@ export {
Link,
RouterProvider,
dynamic,
lazyLoadComponent,
useRouter,
__tuono__internal__lazyLoadComponent,
} from 'tuono-router'
export type { TuonoProps } from './types'
+20 -6
View File
@@ -1,11 +1,16 @@
import 'fast-text-encoding' // Mandatory for React18
import type { ReadableStream } from 'node:stream/web'
import * as React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
import { renderToStaticMarkup, renderToReadableStream } from 'react-dom/server'
import type { HelmetServerState } from 'react-helmet-async'
import { HelmetProvider } from 'react-helmet-async'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
import { MessageChannelPolyfill } from './polyfills/messageChannel'
import { streamToString } from './utils'
type RouteTree = ReturnType<typeof createRoute>
type Mode = 'Dev' | 'Prod'
@@ -37,7 +42,7 @@ function generateJsScripts(jsBundles: Array<string>, mode: Mode): string {
}
export function serverSideRendering(routeTree: RouteTree) {
return function render(payload: string | undefined): string {
return async function render(payload: string | undefined): Promise<string> {
const serverProps = (payload ? JSON.parse(payload) : {}) as Record<
string,
unknown
@@ -48,14 +53,21 @@ export function serverSideRendering(routeTree: RouteTree) {
const cssBundles = serverProps.cssBundles as Array<string>
const router = createRouter({ routeTree }) // Render the app
const helmetContext = {}
const app = renderToString(
const helmetContext = {} as { helmet: HelmetServerState }
const stream = await renderToReadableStream(
<HelmetProvider context={helmetContext}>
<RouterProvider router={router} serverProps={serverProps as never} />
</HelmetProvider>,
)
const { helmet } = helmetContext as { helmet: HelmetServerState }
await stream.allReady
const { helmet } = helmetContext
const app = await streamToString(
// ReadableStream should be implemented in node)
stream as unknown as ReadableStream<Uint8Array>,
)
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
@@ -72,7 +84,7 @@ export function serverSideRendering(routeTree: RouteTree) {
${renderToStaticMarkup(
<script
dangerouslySetInnerHTML={{
__html: `window.__TUONO_SSR_PROPS__=${payload}`,
__html: `window.__TUONO_SSR_PROPS__=${payload as string}`,
}}
/>,
)}
@@ -83,3 +95,5 @@ export function serverSideRendering(routeTree: RouteTree) {
`
}
}
export { MessageChannelPolyfill }
@@ -0,0 +1,150 @@
import { describe, it, expect, vi } from 'vitest'
import { MessageChannelPolyfill, MessagePortPolyfill } from './messageChannel'
describe('MessagePortPolyfill', () => {
it('should invoke onmessage when a message is posted', () => {
const port = new MessagePortPolyfill()
const onmessageMock = vi.fn<EventListener>()
port.onmessage = onmessageMock
port.dispatchEvent({ data: 'Hello, world!' } as MessageEvent)
expect(onmessageMock).toHaveBeenCalledOnce()
expect(onmessageMock).toHaveBeenCalledWith({ data: 'Hello, world!' })
})
it('should handle multiple event listeners', () => {
const port = new MessagePortPolyfill()
const listener1 = vi.fn<EventListener>()
const listener2 = vi.fn<EventListener>()
port.addEventListener('message', listener1)
port.addEventListener('message', listener2)
port.dispatchEvent({ data: 'Test message' } as MessageEvent)
expect(listener1).toHaveBeenCalledOnce()
expect(listener1).toHaveBeenCalledWith({ data: 'Test message' })
expect(listener2).toHaveBeenCalledOnce()
expect(listener2).toHaveBeenCalledWith({ data: 'Test message' })
})
it('should not invoke removed event listeners', () => {
const port = new MessagePortPolyfill()
const listener = vi.fn<EventListener>()
port.addEventListener('message', listener)
port.dispatchEvent({ data: 'First message' } as MessageEvent)
port.removeEventListener('message', listener)
port.dispatchEvent({ data: 'Second message' } as MessageEvent)
expect(listener).toHaveBeenCalledOnce()
expect(listener).toHaveBeenCalledWith({ data: 'First message' })
})
it('should not post messages if otherPort is null', () => {
const port = new MessagePortPolyfill()
const listener = vi.fn<EventListener>()
port.onmessage = listener
port.postMessage('Hello!')
expect(listener).not.toHaveBeenCalledOnce()
})
})
describe('MessageChannelPolyfill', () => {
it('should send and receive messages between ports', () => {
const channel = new MessageChannelPolyfill()
const listener = vi.fn<EventListener>()
channel.port1.onmessage = listener
channel.port2.postMessage('Hello, port1!')
channel.port2.postMessage('How are you?')
expect(listener).toHaveBeenCalledTimes(2)
expect(listener).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ data: 'Hello, port1!' }),
)
expect(listener).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ data: 'How are you?' }),
)
})
it('should support addEventListener and removeEventListener', () => {
const channel = new MessageChannelPolyfill()
const listener = vi.fn<EventListener>()
channel.port1.addEventListener('message', listener)
channel.port2.postMessage('Hello, port1!')
expect(listener).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ data: 'Hello, port1!' }),
)
channel.port1.removeEventListener('message', listener)
channel.port2.postMessage('Message after removing listener')
expect(listener).not.toHaveBeenCalledTimes(2)
})
it('should handle bidirectional communication between ports', () => {
const channel = new MessageChannelPolyfill()
const listener1 = vi.fn<EventListener>()
const listener2 = vi.fn<EventListener>()
channel.port1.onmessage = listener1
channel.port2.onmessage = listener2
channel.port1.postMessage('Hello, port2!')
channel.port2.postMessage('Hello, port1!')
expect(listener1).toHaveBeenCalledOnce()
expect(listener1).toHaveBeenCalledWith(
expect.objectContaining({ data: 'Hello, port1!' }),
)
expect(listener2).toHaveBeenCalledOnce()
expect(listener2).toHaveBeenCalledWith(
expect.objectContaining({ data: 'Hello, port2!' }),
)
})
})
describe('MessagePort', () => {
it('should not send a message on close', () => {
const { port1, port2 } = new MessageChannelPolyfill()
const listener = vi.fn<EventListener>()
port1.onmessage = listener
port2.postMessage('Test message')
expect(listener).toHaveBeenCalledOnce()
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ data: 'Test message' }),
)
port1.close()
port2.postMessage('Another message')
expect(listener).not.toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,102 @@
/* Modified from https://github.com/rocwind/message-port-polyfill/blob/master/src/index.ts
* MIT License
*
* Copyright (c) 2019 Roc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export class MessagePortPolyfill implements MessagePort {
onmessage: ((this: MessagePort, ev: MessageEvent) => unknown) | null = null
/** @warning this is declared to satisfy {@link MessagePort} interface requirements but is never called */
onmessageerror: ((this: MessagePort, ev: MessageEvent) => unknown) | null =
null
otherPort: MessagePortPolyfill | null = null
private onmessageListeners: Array<(ev: MessageEvent) => void> = []
private isClosed = false
dispatchEvent(event: MessageEvent): boolean {
if (this.isClosed) return false
if (this.onmessage) {
this.onmessage(event)
}
this.onmessageListeners.forEach((listener) => {
listener(event)
})
return true
}
postMessage(message: unknown): void {
if (this.isClosed || !this.otherPort) return
const event = new MessageEvent('message', { data: message })
this.otherPort.dispatchEvent(event)
}
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
): void {
if (this.isClosed || type !== 'message') return
if (
typeof listener === 'function' &&
!this.onmessageListeners.includes(listener)
) {
this.onmessageListeners.push(listener)
}
}
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
): void {
if (this.isClosed || type !== 'message') return
if (typeof listener === 'function') {
const index = this.onmessageListeners.indexOf(listener)
if (index !== -1) {
this.onmessageListeners.splice(index, 1)
}
}
}
start(): void {
// do nothing at this moment
}
close(): void {
this.isClosed = true
}
}
export class MessageChannelPolyfill implements MessageChannel {
readonly port1: MessagePortPolyfill
readonly port2: MessagePortPolyfill
constructor() {
this.port1 = new MessagePortPolyfill()
this.port2 = new MessagePortPolyfill()
this.port1.otherPort = this.port2
this.port2.otherPort = this.port1
}
}
+38
View File
@@ -0,0 +1,38 @@
// react ReadableStream type is an empty interface so we are using the one from
// node which match the runtime value
import type { ReadableStream } from 'node:stream/web'
function concatArrayBuffers(chunks: Array<Uint8Array>): Uint8Array {
const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0))
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset)
offset += chunk.length
}
return result
}
async function streamToArrayBuffer(
stream: ReadableStream<Uint8Array>,
): Promise<Uint8Array> {
const chunks: Array<Uint8Array> = []
for await (const chunk of stream) {
chunks.push(chunk)
}
return concatArrayBuffers(chunks)
}
/**
* This function awaits for the whole stream before returning the string.
*
* NOTE: we should improve the bond between the custom V8 runtime and the
* renderToReadableStream React function to return a stream directly to the client.
*/
export async function streamToString(
stream: ReadableStream<Uint8Array>,
): Promise<string> {
const buffer = await streamToArrayBuffer(stream)
return new TextDecoder().decode(buffer)
}
+51 -284
View File
@@ -32,12 +32,9 @@ importers:
eslint-plugin-import:
specifier: ^2.31.0
version: 2.31.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@9.15.0)
eslint-plugin-react:
specifier: ^7.33.2
version: 7.37.2(eslint@9.15.0)
eslint-plugin-react-hooks:
specifier: ^5.0.0
version: 5.0.0(eslint@9.15.0)
specifier: 5.1.0
version: 5.1.0(eslint@9.15.0)
prettier:
specifier: ^3.2.4
version: 3.4.1
@@ -84,8 +81,8 @@ importers:
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
tuono:
specifier: npm:tuono@0.16.0
version: 0.16.0(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49))
specifier: npm:tuono@0.16.4
version: 0.16.4(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49))
devDependencies:
'@mdx-js/rollup':
specifier: 3.1.0
@@ -294,6 +291,9 @@ importers:
'@babel/types':
specifier: ^7.24.0
version: 7.26.0
'@rollup/plugin-inject':
specifier: ^5.0.5
version: 5.0.5(rollup@4.25.0)
'@types/babel__core':
specifier: ^7.20.5
version: 7.20.5
@@ -327,6 +327,9 @@ importers:
vite:
specifier: ^5.2.11
version: 5.4.11(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
web-streams-polyfill:
specifier: ^4.0.0
version: 4.0.0
devDependencies:
'@types/babel-traverse':
specifier: ^6.25.10
@@ -799,6 +802,15 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@rollup/plugin-inject@5.0.5':
resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.1.3':
resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
engines: {node: '>=14.0.0'}
@@ -1308,10 +1320,6 @@ packages:
resolution: {integrity: sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==}
engines: {node: '>=0.10.0'}
array.prototype.findlast@1.2.5:
resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
engines: {node: '>= 0.4'}
array.prototype.findlastindex@1.2.5:
resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==}
engines: {node: '>= 0.4'}
@@ -1324,10 +1332,6 @@ packages:
resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==}
engines: {node: '>= 0.4'}
array.prototype.tosorted@1.1.4:
resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
engines: {node: '>= 0.4'}
arraybuffer.prototype.slice@1.0.3:
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
engines: {node: '>= 0.4'}
@@ -1612,10 +1616,6 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-iterator-helpers@1.2.0:
resolution: {integrity: sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==}
engines: {node: '>= 0.4'}
es-module-lexer@1.5.4:
resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==}
@@ -1705,18 +1705,12 @@ packages:
'@typescript-eslint/parser':
optional: true
eslint-plugin-react-hooks@5.0.0:
resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==}
eslint-plugin-react-hooks@5.1.0:
resolution: {integrity: sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==}
engines: {node: '>=10'}
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
eslint-plugin-react@7.37.2:
resolution: {integrity: sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==}
engines: {node: '>=4'}
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
eslint-scope@8.2.0:
resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2062,10 +2056,6 @@ packages:
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
engines: {node: '>= 0.4'}
is-async-function@2.0.0:
resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==}
engines: {node: '>= 0.4'}
is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
@@ -2099,13 +2089,6 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-finalizationregistry@1.0.2:
resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==}
is-generator-function@1.0.10:
resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==}
engines: {node: '>= 0.4'}
is-git-repository@1.1.1:
resolution: {integrity: sha512-hxLpJytJnIZ5Og5QsxSkzmb8Qx8rGau9bio1JN/QtXcGEFuSsQYau0IiqlsCwftsfVYjF1mOq6uLdmwNSspgpA==}
@@ -2116,10 +2099,6 @@ packages:
is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
is-map@2.0.3:
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
engines: {node: '>= 0.4'}
is-negative-zero@2.0.3:
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
engines: {node: '>= 0.4'}
@@ -2155,10 +2134,6 @@ packages:
resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==}
engines: {node: '>=0.10.0'}
is-set@2.0.3:
resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
engines: {node: '>= 0.4'}
is-shared-array-buffer@1.0.3:
resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
engines: {node: '>= 0.4'}
@@ -2187,17 +2162,9 @@ packages:
resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==}
engines: {node: '>=0.10.0'}
is-weakmap@2.0.2:
resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
engines: {node: '>= 0.4'}
is-weakref@1.0.2:
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
is-weakset@2.0.3:
resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==}
engines: {node: '>= 0.4'}
is-windows@1.0.2:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
@@ -2212,10 +2179,6 @@ packages:
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
engines: {node: '>=0.10.0'}
iterator.prototype@1.1.3:
resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==}
engines: {node: '>= 0.4'}
jju@1.4.0:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
@@ -2268,10 +2231,6 @@ packages:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -2509,10 +2468,6 @@ packages:
nwsapi@2.2.13:
resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.3:
resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
engines: {node: '>= 0.4'}
@@ -2529,10 +2484,6 @@ packages:
resolution: {integrity: sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==}
engines: {node: '>=0.10.0'}
object.entries@1.1.8:
resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==}
engines: {node: '>= 0.4'}
object.fromentries@2.0.8:
resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
engines: {node: '>= 0.4'}
@@ -2685,9 +2636,6 @@ packages:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
@@ -2723,9 +2671,6 @@ packages:
react-dom:
optional: true
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -2799,10 +2744,6 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
reflect.getprototypeof@1.0.6:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
@@ -2840,10 +2781,6 @@ packages:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
resolve@2.0.0-next.5:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true
reusify@1.0.4:
resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -2968,13 +2905,6 @@ packages:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string.prototype.matchall@4.0.11:
resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==}
engines: {node: '>= 0.4'}
string.prototype.repeat@1.0.0:
resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
string.prototype.trim@1.2.9:
resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==}
engines: {node: '>= 0.4'}
@@ -3111,20 +3041,20 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tuono-fs-router-vite-plugin@0.16.0:
resolution: {integrity: sha512-NjMNhCp9vqladhelMI8Whhp6qhwvoyBRUUyeJpqfRVp/W/vwL/+za6iciscWQ1+yVMEDjY5lnMGA0yQuz0cnIg==}
tuono-fs-router-vite-plugin@0.16.4:
resolution: {integrity: sha512-GZtHp7avh/Ne4Km7XtBNi3Es41v/3EjBvmd4cYwbNvwCcckDXRv8gVZZSQ7lTAdq1WLH0aWrHb/xcocMM1eX6Q==}
tuono-lazy-fn-vite-plugin@0.16.0:
resolution: {integrity: sha512-+ab7pbRrqGqYPePHdPvMGZMWsnEJi2TPTvkHXwV5dcflpRvhhrCppC7QbG5lk24HD+hCFryWpOiZlfzf/j6p+w==}
tuono-lazy-fn-vite-plugin@0.16.4:
resolution: {integrity: sha512-uUomBxRKjEhTtNA1B3VhuUd1EOor2CsGjD8s7nPN08vPDZ2KiUXN1bpXlGYdGPByVBAgILZtPpggZTCh0A9Mhw==}
tuono-router@0.16.0:
resolution: {integrity: sha512-JuTXoSjjgKCbFWJQTRQA2NyN9fHqasXrE1+vC3TV1PJ5P66EM2f4zlAzwMkUNbeGWCl/Kihi/gKT1pVqnieUFA==}
tuono-router@0.16.4:
resolution: {integrity: sha512-BC5urCZQDl4QJ8CPJe48d9wWgVWBuyfZCSLHvFBURoYKYAqg3Qo8W8w8xcCM+qQq4IFZU8ClgVPOeqbXQ34f8A==}
peerDependencies:
react: '>=16.3.0'
react-dom: '>=16.3.0'
tuono@0.16.0:
resolution: {integrity: sha512-VFuRPsIf3V3UMlTtgVo1/EgKruJRjNMFkahF88a9he1b7TS0FFTlw/AEC8D92NmkB6PFjyH7g9ZNw8bpogPTmQ==}
tuono@0.16.4:
resolution: {integrity: sha512-ym2xjwojC8/3vE3jGTBCVgrykvZ3EIBs63o+wuG4DJ4+MaK5j36vCvmAz9U6oURyNGRS0lYpr8PKOXzB9CCT0w==}
hasBin: true
peerDependencies:
react: '>=16.3.0'
@@ -3423,6 +3353,10 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
web-streams-polyfill@4.0.0:
resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==}
engines: {node: '>= 8'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -3442,14 +3376,6 @@ packages:
which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
which-builtin-type@1.1.4:
resolution: {integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==}
engines: {node: '>= 0.4'}
which-collection@1.0.2:
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'}
which-typed-array@1.1.15:
resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
engines: {node: '>= 0.4'}
@@ -4031,6 +3957,14 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@rollup/plugin-inject@5.0.5(rollup@4.25.0)':
dependencies:
'@rollup/pluginutils': 5.1.3(rollup@4.25.0)
estree-walker: 2.0.2
magic-string: 0.30.14
optionalDependencies:
rollup: 4.25.0
'@rollup/pluginutils@5.1.3(rollup@4.25.0)':
dependencies:
'@types/estree': 1.0.6
@@ -4618,15 +4552,6 @@ snapshots:
array-slice@1.1.0: {}
array.prototype.findlast@1.2.5:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-object-atoms: 1.0.0
es-shim-unscopables: 1.0.2
array.prototype.findlastindex@1.2.5:
dependencies:
call-bind: 1.0.7
@@ -4650,14 +4575,6 @@ snapshots:
es-abstract: 1.23.3
es-shim-unscopables: 1.0.2
array.prototype.tosorted@1.1.4:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-shim-unscopables: 1.0.2
arraybuffer.prototype.slice@1.0.3:
dependencies:
array-buffer-byte-length: 1.0.1
@@ -4967,24 +4884,6 @@ snapshots:
es-errors@1.3.0: {}
es-iterator-helpers@1.2.0:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-set-tostringtag: 2.0.3
function-bind: 1.1.2
get-intrinsic: 1.2.4
globalthis: 1.0.4
gopd: 1.0.1
has-property-descriptors: 1.0.2
has-proto: 1.0.3
has-symbols: 1.0.3
internal-slot: 1.0.7
iterator.prototype: 1.1.3
safe-array-concat: 1.1.2
es-module-lexer@1.5.4: {}
es-object-atoms@1.0.0:
@@ -5125,32 +5024,10 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-react-hooks@5.0.0(eslint@9.15.0):
eslint-plugin-react-hooks@5.1.0(eslint@9.15.0):
dependencies:
eslint: 9.15.0
eslint-plugin-react@7.37.2(eslint@9.15.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlast: 1.2.5
array.prototype.flatmap: 1.3.2
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.2.0
eslint: 9.15.0
estraverse: 5.3.0
hasown: 2.0.2
jsx-ast-utils: 3.3.5
minimatch: 3.1.2
object.entries: 1.1.8
object.fromentries: 2.0.8
object.values: 1.2.0
prop-types: 15.8.1
resolve: 2.0.0-next.5
semver: 6.3.1
string.prototype.matchall: 4.0.11
string.prototype.repeat: 1.0.0
eslint-scope@8.2.0:
dependencies:
esrecurse: 4.3.0
@@ -5571,10 +5448,6 @@ snapshots:
call-bind: 1.0.7
get-intrinsic: 1.2.4
is-async-function@2.0.0:
dependencies:
has-tostringtag: 1.0.2
is-bigint@1.0.4:
dependencies:
has-bigints: 1.0.2
@@ -5606,14 +5479,6 @@ snapshots:
is-extglob@2.1.1: {}
is-finalizationregistry@1.0.2:
dependencies:
call-bind: 1.0.7
is-generator-function@1.0.10:
dependencies:
has-tostringtag: 1.0.2
is-git-repository@1.1.1:
dependencies:
execa: 0.6.3
@@ -5625,8 +5490,6 @@ snapshots:
is-hexadecimal@2.0.1: {}
is-map@2.0.3: {}
is-negative-zero@2.0.3: {}
is-number-object@1.0.7:
@@ -5652,8 +5515,6 @@ snapshots:
dependencies:
is-unc-path: 1.0.0
is-set@2.0.3: {}
is-shared-array-buffer@1.0.3:
dependencies:
call-bind: 1.0.7
@@ -5680,17 +5541,10 @@ snapshots:
dependencies:
unc-path-regex: 0.1.2
is-weakmap@2.0.2: {}
is-weakref@1.0.2:
dependencies:
call-bind: 1.0.7
is-weakset@2.0.3:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
is-windows@1.0.2: {}
isarray@2.0.5: {}
@@ -5699,14 +5553,6 @@ snapshots:
isobject@3.0.1: {}
iterator.prototype@1.1.3:
dependencies:
define-properties: 1.2.1
get-intrinsic: 1.2.4
has-symbols: 1.0.3
reflect.getprototypeof: 1.0.6
set-function-name: 2.0.2
jju@1.4.0: {}
js-tokens@4.0.0: {}
@@ -5769,13 +5615,6 @@ snapshots:
jsonparse@1.3.1: {}
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.8
array.prototype.flat: 1.3.2
object.assign: 4.1.5
object.values: 1.2.0
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -6194,8 +6033,6 @@ snapshots:
nwsapi@2.2.13: {}
object-assign@4.1.1: {}
object-inspect@1.13.3: {}
object-keys@1.1.1: {}
@@ -6214,12 +6051,6 @@ snapshots:
for-own: 1.0.0
isobject: 3.0.1
object.entries@1.1.8:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-object-atoms: 1.0.0
object.fromentries@2.0.8:
dependencies:
call-bind: 1.0.7
@@ -6368,12 +6199,6 @@ snapshots:
ansi-styles: 5.2.0
react-is: 17.0.2
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
property-information@6.5.0: {}
pseudomap@1.0.2: {}
@@ -6403,8 +6228,6 @@ snapshots:
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
react-is@16.13.1: {}
react-is@17.0.2: {}
react-number-format@5.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -6494,16 +6317,6 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
reflect.getprototypeof@1.0.6:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
get-intrinsic: 1.2.4
globalthis: 1.0.4
which-builtin-type: 1.1.4
regenerator-runtime@0.14.1: {}
regexp.prototype.flags@1.5.3:
@@ -6565,12 +6378,6 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
resolve@2.0.0-next.5:
dependencies:
is-core-module: 2.15.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.0.4: {}
rollup-plugin-preserve-directives@0.4.0(rollup@4.25.0):
@@ -6707,26 +6514,6 @@ snapshots:
string-argv@0.3.2: {}
string.prototype.matchall@4.0.11:
dependencies:
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-errors: 1.3.0
es-object-atoms: 1.0.0
get-intrinsic: 1.2.4
gopd: 1.0.1
has-symbols: 1.0.3
internal-slot: 1.0.7
regexp.prototype.flags: 1.5.3
set-function-name: 2.0.2
side-channel: 1.0.6
string.prototype.repeat@1.0.0:
dependencies:
define-properties: 1.2.1
es-abstract: 1.23.3
string.prototype.trim@1.2.9:
dependencies:
call-bind: 1.0.7
@@ -6846,7 +6633,7 @@ snapshots:
tslib@2.8.1: {}
tuono-fs-router-vite-plugin@0.16.0(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49)):
tuono-fs-router-vite-plugin@0.16.4(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49)):
dependencies:
'@babel/core': 7.26.0
'@babel/types': 7.26.0
@@ -6863,7 +6650,7 @@ snapshots:
- supports-color
- terser
tuono-lazy-fn-vite-plugin@0.16.0(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49)):
tuono-lazy-fn-vite-plugin@0.16.4(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49)):
dependencies:
'@babel/core': 7.26.0
'@babel/types': 7.26.0
@@ -6879,7 +6666,7 @@ snapshots:
- supports-color
- terser
tuono-router@0.16.0(@types/node@22.10.0)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49)):
tuono-router@0.16.4(@types/node@22.10.0)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49)):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -6898,7 +6685,7 @@ snapshots:
- sugarss
- terser
tuono@0.16.0(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49)):
tuono@0.16.4(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49)):
dependencies:
'@babel/core': 7.26.0
'@babel/generator': 7.26.2
@@ -6916,9 +6703,9 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-helmet-async: 2.0.5(react@18.3.1)
tuono-fs-router-vite-plugin: 0.16.0(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-lazy-fn-vite-plugin: 0.16.0(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-router: 0.16.0(@types/node@22.10.0)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49))
tuono-fs-router-vite-plugin: 0.16.4(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-lazy-fn-vite-plugin: 0.16.4(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-router: 0.16.4(@types/node@22.10.0)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sugarss@4.0.1(postcss@8.4.49))
vite: 5.4.11(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
transitivePeerDependencies:
- '@swc/helpers'
@@ -7276,6 +7063,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
web-streams-polyfill@4.0.0: {}
webidl-conversions@7.0.0: {}
whatwg-encoding@3.1.1:
@@ -7297,28 +7086,6 @@ snapshots:
is-string: 1.0.7
is-symbol: 1.0.4
which-builtin-type@1.1.4:
dependencies:
function.prototype.name: 1.1.6
has-tostringtag: 1.0.2
is-async-function: 2.0.0
is-date-object: 1.0.5
is-finalizationregistry: 1.0.2
is-generator-function: 1.0.10
is-regex: 1.1.4
is-weakref: 1.0.2
isarray: 2.0.5
which-boxed-primitive: 1.0.2
which-collection: 1.0.2
which-typed-array: 1.1.15
which-collection@1.0.2:
dependencies:
is-map: 2.0.3
is-set: 2.0.3
is-weakmap: 2.0.2
is-weakset: 2.0.3
which-typed-array@1.1.15:
dependencies:
available-typed-arrays: 1.0.7