diff --git a/Cargo.toml b/Cargo.toml index f5a3efbc..5e8613a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "crates/tuono_lib_macros", ] exclude = [ - "examples/playground", + "examples/mdx", "examples/tuono", "examples/tutorial" ] diff --git a/README.md b/README.md index 6b4bdd84..2e783447 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ by Tuono based on the files defined within the `./src/routes` directory. - 🍭 CSS modules - 🧬 Server Side Rendering - 🧵 Multi thread backend +- ⌨️ MDX support - ⚙️ Build optimizations - Custom APIs* - Image optimization* diff --git a/crates/tuono/Cargo.toml b/crates/tuono/Cargo.toml index 02250cc9..aab19b55 100644 --- a/crates/tuono/Cargo.toml +++ b/crates/tuono/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono" -version = "0.6.0" +version = "0.7.0" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" diff --git a/crates/tuono/src/source_builder.rs b/crates/tuono/src/source_builder.rs index 6065ff5b..b6227564 100644 --- a/crates/tuono/src/source_builder.rs +++ b/crates/tuono/src/source_builder.rs @@ -33,7 +33,8 @@ pub const AXUM_ENTRY_POINT: &str = r##" // File automatically generated // Do not manually change it -use tuono_lib::{tokio, Mode, Server, axum::Router, axum::routing::get}; +use tuono_lib::{tokio, Mode, Server, axum::Router}; +// AXUM_GET_ROUTE_HANDLER const MODE: Mode = /*MODE*/; @@ -116,17 +117,32 @@ pub fn bundle_axum_source(mode: Mode) -> io::Result<()> { Ok(()) } -fn generate_axum_source(source_builder: &App, mode: Mode) -> String { - AXUM_ENTRY_POINT +fn generate_axum_source(app: &App, mode: Mode) -> String { + let src = AXUM_ENTRY_POINT .replace( "// ROUTE_BUILDER\n", - &create_routes_declaration(&source_builder.route_map), + &create_routes_declaration(&app.route_map), ) .replace( "// MODULE_IMPORTS\n", - &create_modules_declaration(&source_builder.route_map), + &create_modules_declaration(&app.route_map), ) - .replace("/*MODE*/", mode.as_str()) + .replace("/*MODE*/", mode.as_str()); + + let has_server_handlers = app + .route_map + .iter() + .filter(|(_, route)| route.axum_info.is_some()) + .collect::>() + .is_empty(); + + if !has_server_handlers { + return src.replace( + "// AXUM_GET_ROUTE_HANDLER", + "use tuono_lib::axum::routing::get;", + ); + } + src } pub fn check_tuono_folder() -> io::Result<()> { @@ -166,4 +182,27 @@ mod tests { assert!(prod_bundle.contains("const MODE: Mode = Mode::Prod;")); } + + #[test] + fn should_not_load_the_axum_get_function() { + let source_builder = App::new(); + + let dev_bundle = generate_axum_source(&source_builder, Mode::Dev); + assert!(!dev_bundle.contains("use tuono_lib::axum::routing::get;")); + } + + #[test] + fn should_load_the_axum_get_function() { + let mut source_builder = App::new(); + + let mut route = Route::new(String::from("index.tsx")); + route.update_axum_info(); + + source_builder + .route_map + .insert(String::from("index.rs"), route); + + let dev_bundle = generate_axum_source(&source_builder, Mode::Dev); + assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;")); + } } diff --git a/crates/tuono_lib/Cargo.toml b/crates/tuono_lib/Cargo.toml index c7061031..10277b93 100644 --- a/crates/tuono_lib/Cargo.toml +++ b/crates/tuono_lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib" -version = "0.6.0" +version = "0.7.0" edition = "2021" authors = ["V. Ageno "] description = "The react/rust fullstack framework" @@ -32,7 +32,7 @@ regex = "1.10.5" either = "1.13.0" tower-http = {version = "0.5.2", features = ["fs"]} -tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.6.0"} +tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.7.0"} # Match the same version used by axum tokio-tungstenite = "0.21.0" futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } diff --git a/crates/tuono_lib_macros/Cargo.toml b/crates/tuono_lib_macros/Cargo.toml index dcc3e6e6..6229b5ed 100644 --- a/crates/tuono_lib_macros/Cargo.toml +++ b/crates/tuono_lib_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuono_lib_macros" -version = "0.6.0" +version = "0.7.0" edition = "2021" description = "The react/rust fullstack framework" keywords = [ "react", "typescript", "fullstack", "web", "ssr"] diff --git a/examples/README.md b/examples/README.md index 94feec77..cf8a0f31 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,4 +16,13 @@ $ tuono new [NAME] --template [TEMPLATE] `[TEMPLATE]` is the folder name. +## Troubleshooting for contributors +There is a common error that happens during example development due to +pnpm workspace about the findings of different react versions. + +To overcome this issue run within the single example folder: + +``` +pnpm install --ignore-workspace && pnpm upgrade --save +``` diff --git a/examples/mdx/.gitignore b/examples/mdx/.gitignore new file mode 100644 index 00000000..071acad0 --- /dev/null +++ b/examples/mdx/.gitignore @@ -0,0 +1,13 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.tuono +out +target diff --git a/examples/mdx/Cargo.toml b/examples/mdx/Cargo.toml new file mode 100644 index 00000000..e4487952 --- /dev/null +++ b/examples/mdx/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tuono" +version = "0.0.1" +edition = "2021" + +[[bin]] +name = "tuono" +path = ".tuono/main.rs" + +[dependencies] +tuono_lib = { path = "../../crates/tuono_lib/"} +serde = { version = "1.0.202", features = ["derive"] } + diff --git a/examples/mdx/README.md b/examples/mdx/README.md new file mode 100644 index 00000000..72b0fe6d --- /dev/null +++ b/examples/mdx/README.md @@ -0,0 +1,9 @@ +# Tuono starter + +This is the starter tuono project. To download it run in your terminal: + +```shell +$ tuono new [NAME] +``` + + diff --git a/examples/mdx/package.json b/examples/mdx/package.json new file mode 100644 index 00000000..8c47d305 --- /dev/null +++ b/examples/mdx/package.json @@ -0,0 +1,16 @@ +{ + "name": "tuono", + "description": "The react/rust fullstack framework", + "version": "0.0.1", + "dependencies": { + "@mdx-js/react": "^3.0.1", + "react": "18.3.1", + "react-dom": "18.3.1", + "tuono": "link:../../packages/tuono" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.5.3" + } +} diff --git a/examples/mdx/public/favicon.ico b/examples/mdx/public/favicon.ico new file mode 100644 index 00000000..d03bcc66 Binary files /dev/null and b/examples/mdx/public/favicon.ico differ diff --git a/examples/mdx/src/routes/__root.tsx b/examples/mdx/src/routes/__root.tsx new file mode 100644 index 00000000..b4a0ad50 --- /dev/null +++ b/examples/mdx/src/routes/__root.tsx @@ -0,0 +1,14 @@ +import type { ReactNode } from 'react' +import { MDXProvider } from '@mdx-js/react' + +interface RootRouteProps { + children: ReactNode +} + +export default function RootRoute({ children }: RootRouteProps): JSX.Element { + return ( +
+ {children} +
+ ) +} diff --git a/examples/mdx/src/routes/index.mdx b/examples/mdx/src/routes/index.mdx new file mode 100644 index 00000000..f1ecb3da --- /dev/null +++ b/examples/mdx/src/routes/index.mdx @@ -0,0 +1 @@ +# Index page diff --git a/examples/mdx/src/styles/global.css b/examples/mdx/src/styles/global.css new file mode 100644 index 00000000..84fc0842 --- /dev/null +++ b/examples/mdx/src/styles/global.css @@ -0,0 +1,109 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Poppins', sans-serif; + font-weight: 400; + font-style: normal; +} + +body { + background: #fbfbfb; +} + +main { + width: 673px; + margin: 20px auto; +} + +.header { + display: flex; + gap: 20px; +} + +.header a { + color: black; + font-size: 18px; + font-weight: 600; + text-decoration: none; + z-index: 2; +} + +.title-wrap { + height: 200px; +} + +.title { + position: absolute; + font-size: 200px; + line-height: 200px; + z-index: 0; + letter-spacing: -2px; + margin-left: -8px; + user-select: none; + pointer-events: none; +} + +.title span { + opacity: 0; +} + +.button { + width: 140px; + height: 30px; + border: solid 3px black; + border-radius: 10px; + color: black; + text-decoration: none; + display: flex; + justify-content: center; + align-items: center; + transition: 0.2s; +} + +.button:hover { + color: white; + background: black; +} + +.logo { + margin-left: 240px; + position: relative; + top: 25px; +} + +.logo img { + position: absolute; +} + +.rust { + animation: rotate 6s ease-in-out infinite; +} + +.react { + top: 33px; + left: 28px; + animation: rotate 6s linear infinite reverse; +} + +.subtitle { + font-size: 30px; + line-height: 30px; + letter-spacing: -1px; +} + +.subtitle-wrap { + display: flex; + justify-content: space-between; +} diff --git a/examples/mdx/tsconfig.json b/examples/mdx/tsconfig.json new file mode 100644 index 00000000..a7fc6fbf --- /dev/null +++ b/examples/mdx/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/examples/mdx/tsconfig.node.json b/examples/mdx/tsconfig.node.json new file mode 100644 index 00000000..97ede7ee --- /dev/null +++ b/examples/mdx/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/package.json b/package.json index 9b4361c0..7713368e 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "format": "turbo format --filter tuono", "format:check": "turbo format:check --filter tuono", "types": "turbo types --filter tuono", - "test": "turbo test --filter tuono", - "test:watch": "turbo test:watch --filter tuono" + "test": "turbo test", + "test:watch": "turbo test:watch" }, "workspaces": [ "tuono", diff --git a/packages/fs-router-vite-plugin/package.json b/packages/fs-router-vite-plugin/package.json index c3279abc..65713c8d 100644 --- a/packages/fs-router-vite-plugin/package.json +++ b/packages/fs-router-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-fs-router-vite-plugin", - "version": "0.6.0", + "version": "0.7.0", "description": "Plugin for the tuono's file system router. Tuono is the react/rust fullstack framework", "scripts": { "dev": "vite build --watch", diff --git a/packages/fs-router-vite-plugin/src/generator.ts b/packages/fs-router-vite-plugin/src/generator.ts index 4af951a9..a61ad2e5 100644 --- a/packages/fs-router-vite-plugin/src/generator.ts +++ b/packages/fs-router-vite-plugin/src/generator.ts @@ -46,7 +46,7 @@ async function getRouteNodes( if (dirent.isDirectory()) { await recurse(relativePath) - } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) { + } else if (fullPath.match(/\.(tsx|ts|jsx|js|mdx)$/)) { const filePath = replaceBackslash(path.join(dir, dirent.name)) const filePathNoExt = removeExt(filePath) let routePath = @@ -220,6 +220,7 @@ export async function routeGenerator(config = defaultConfig): Promise { const imports = [ ...sortedRouteNodes.map((node) => { + const extension = node.filePath.endsWith('mdx') ? '.mdx' : '' return `const ${ node.variableName }Import = dynamic(() => import('./${replaceBackslash( @@ -230,7 +231,7 @@ export async function routeGenerator(config = defaultConfig): Promise { ), false, ), - )}'))` + )}${extension}'))` }), ].join('\n') diff --git a/packages/fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts b/packages/fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts new file mode 100644 index 00000000..576d6abb --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/mdx/routeTree.expected.ts @@ -0,0 +1,29 @@ +// This file is auto-generated by Tuono + +import { createRoute, dynamic } from 'tuono' + +import RootImport from './routes/__root' + +const AboutImport = dynamic(() => import('./routes/about.mdx')) +const IndexImport = dynamic(() => import('./routes/index')) + +const rootRoute = createRoute({ isRoot: true, component: RootImport }) + +const About = createRoute({ component: AboutImport }) +const Index = createRoute({ component: IndexImport }) + +// Create/Update Routes + +const AboutRoute = About.update({ + path: '/about', + getParentRoute: () => rootRoute, +}) + +const IndexRoute = Index.update({ + path: '/', + getParentRoute: () => rootRoute, +}) + +// Create and export the route tree + +export const routeTree = rootRoute.addChildren([IndexRoute, AboutRoute]) diff --git a/packages/fs-router-vite-plugin/tests/generator/mdx/routes/__root.tsx b/packages/fs-router-vite-plugin/tests/generator/mdx/routes/__root.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/mdx/routes/__root.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/fs-router-vite-plugin/tests/generator/mdx/routes/about.mdx b/packages/fs-router-vite-plugin/tests/generator/mdx/routes/about.mdx new file mode 100644 index 00000000..e69de29b diff --git a/packages/fs-router-vite-plugin/tests/generator/mdx/routes/index.tsx b/packages/fs-router-vite-plugin/tests/generator/mdx/routes/index.tsx new file mode 100644 index 00000000..ab504e42 --- /dev/null +++ b/packages/fs-router-vite-plugin/tests/generator/mdx/routes/index.tsx @@ -0,0 +1 @@ +/** */ diff --git a/packages/lazy-fn-vite-plugin/package.json b/packages/lazy-fn-vite-plugin/package.json index 3b01e33b..ab067ad2 100644 --- a/packages/lazy-fn-vite-plugin/package.json +++ b/packages/lazy-fn-vite-plugin/package.json @@ -1,6 +1,6 @@ { "name": "tuono-lazy-fn-vite-plugin", - "version": "0.6.0", + "version": "0.7.0", "description": "Plugin for the tuono's lazy fn. Tuono is the react/rust fullstack framework", "scripts": { "dev": "vite build --watch", diff --git a/packages/tuono/package.json b/packages/tuono/package.json index d029e747..bd5f0e33 100644 --- a/packages/tuono/package.json +++ b/packages/tuono/package.json @@ -1,6 +1,6 @@ { "name": "tuono", - "version": "0.6.0", + "version": "0.7.0", "description": "The react/rust fullstack framework", "scripts": { "dev": "vite build --watch", @@ -84,6 +84,7 @@ "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.1", "@babel/types": "^7.24.0", + "@mdx-js/rollup": "^3.0.1", "@types/babel__core": "^7.20.5", "@types/node": "^20.12.7", "@vitejs/plugin-react-swc": "^3.7.0", diff --git a/packages/tuono/src/build/index.ts b/packages/tuono/src/build/index.ts index 29d3da12..4810469c 100644 --- a/packages/tuono/src/build/index.ts +++ b/packages/tuono/src/build/index.ts @@ -2,6 +2,7 @@ import { build, createServer, InlineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' import ViteFsRouter from 'tuono-fs-router-vite-plugin' import { LazyLoadingPlugin } from 'tuono-lazy-fn-vite-plugin' +import mdx from '@mdx-js/rollup' const BASE_CONFIG: InlineConfig = { root: '.tuono', @@ -9,7 +10,12 @@ const BASE_CONFIG: InlineConfig = { publicDir: '../public', cacheDir: 'cache', envDir: '../', - plugins: [react(), ViteFsRouter(), LazyLoadingPlugin()], + plugins: [ + { enforce: 'pre', ...mdx() }, + react(), + ViteFsRouter(), + LazyLoadingPlugin(), + ], } const VITE_PORT = 3001