Compare commits

...

11 Commits

Author SHA1 Message Date
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
47 changed files with 390 additions and 201 deletions
+6 -1
View File
@@ -1 +1,6 @@
pnpm-lock.yaml
pnpm-lock.yaml
dist
.tuono
packages/lazy-fn-vite-plugin/tests/sources/*
+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.2"
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.2"
},
"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',
@@ -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.4"
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.4"
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.4"}
# 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.4"
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;
+1
View File
@@ -14,6 +14,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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tuono-fs-router-vite-plugin",
"version": "0.16.2",
"version": "0.16.4",
"description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework",
"homepage": "https://tuono.dev",
"scripts": {
@@ -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.4",
"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.4",
"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"
+3 -1
View File
@@ -41,7 +41,9 @@ export const dynamic = (importFn: ImportFn): React.JSX.Element => {
return <></>
}
export const lazyLoadComponent = (factory: ImportFn): RouteComponent => {
export const __tuono__internal__lazyLoadComponent = (
factory: ImportFn,
): RouteComponent => {
let LoadedComponent: ComponentType<any> | undefined
const LazyComponent = React.lazy(factory) as unknown as RouteComponent
+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'
-2
View File
@@ -1,2 +0,0 @@
dist
pnpm-lock.yaml
+3 -3
View File
@@ -1,14 +1,14 @@
{
"name": "tuono",
"version": "0.16.2",
"version": "0.16.4",
"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"
+8 -2
View File
@@ -17,6 +17,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 +47,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 }),
],
}
+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'
+17 -17
View File
@@ -84,8 +84,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.2
version: 0.16.2(@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
@@ -3111,20 +3111,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.2:
resolution: {integrity: sha512-D0SHBX4IT1eQGeNNmXUYpnAtwVPutmnboMGRc0Tsf6tK2PjHY4Q10X2vUwFXm9Ps1mg0DHdAHl/90b4mtvG6Xg==}
tuono-lazy-fn-vite-plugin@0.16.0:
resolution: {integrity: sha512-+ab7pbRrqGqYPePHdPvMGZMWsnEJi2TPTvkHXwV5dcflpRvhhrCppC7QbG5lk24HD+hCFryWpOiZlfzf/j6p+w==}
tuono-lazy-fn-vite-plugin@0.16.2:
resolution: {integrity: sha512-LFIO73qG9ckKdPmtT79vnwjcnFiRm2ntyoJU2Ce3/BNPVgDR1McITjEkeWwSwwRXiMDTX1cb7uDabprZcV8qSg==}
tuono-router@0.16.0:
resolution: {integrity: sha512-JuTXoSjjgKCbFWJQTRQA2NyN9fHqasXrE1+vC3TV1PJ5P66EM2f4zlAzwMkUNbeGWCl/Kihi/gKT1pVqnieUFA==}
tuono-router@0.16.2:
resolution: {integrity: sha512-NhRbICCjF9l/1saRbvFOoso342FBJC6+Cx4rzFdHnY+uPqBd+SrDjydDpw1b+uqoHj1p5Y+QcXNIEqHRK1ajww==}
peerDependencies:
react: '>=16.3.0'
react-dom: '>=16.3.0'
tuono@0.16.0:
resolution: {integrity: sha512-VFuRPsIf3V3UMlTtgVo1/EgKruJRjNMFkahF88a9he1b7TS0FFTlw/AEC8D92NmkB6PFjyH7g9ZNw8bpogPTmQ==}
tuono@0.16.2:
resolution: {integrity: sha512-Cvs5ZMezUIZAU++F9/qMWfU0gN1BSF+o3yBw9YpYuIbWhsLWbIzgTUHs+kKOIGBlpgjARldwXDnWMPMz0m2h7A==}
hasBin: true
peerDependencies:
react: '>=16.3.0'
@@ -6846,7 +6846,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.2(@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 +6863,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.2(@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 +6879,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.2(@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 +6898,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.2(@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 +6916,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.2(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-lazy-fn-vite-plugin: 0.16.2(@types/node@22.10.0)(sugarss@4.0.1(postcss@8.4.49))
tuono-router: 0.16.2(@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'