Compare commits

...

432 Commits

Author SHA1 Message Date
Valerio Ageno be77068c73 chore: update version to v0.17.8 (#559) 2025-02-12 18:34:06 +01:00
Marco Pasqualetti 907c92bedc chore: remove documentation (#556) 2025-02-12 17:01:35 +01:00
Jovan Milosevic 6794ab20d3 feat: Add e2e setup and tests (#535) 2025-02-12 08:57:18 +01:00
Valerio Ageno e00dad9200 feat: polyfill URLSearchParams on the server side (#555) 2025-02-12 08:35:36 +01:00
Valerio Ageno 9aa6aedee0 feat: add spinner to 'tuono dev' (#550) 2025-02-11 17:21:28 +01:00
renovate[bot] 0114867fe6 chore(deps): update dependency typescript-eslint to v8.24.0 (#551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 20:29:29 +01:00
renovate[bot] 4f58d42582 chore(deps): update devdependencies (eslint) to v9.20.0 (#546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 17:44:23 +01:00
Marco Pasqualetti 40add60fd8 chore(pnpm): dedupe dependencies (#547) 2025-02-10 17:31:42 +01:00
renovate[bot] 9a4bad95ed chore(deps): update dependency prettier to v3.5.0 (#545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 09:15:42 +01:00
renovate[bot] 3e28aeea27 fix(deps): update dependency posthog-js to v1.215.7 (#543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 08:56:18 +01:00
renovate[bot] 58e48147b8 chore(deps): update dependency @types/node to v22.13.1 (#541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 08:36:19 +01:00
Marco Pasqualetti 68eda84053 test(packages/tuono-router): use screen instead of render matchers (#540) 2025-02-09 19:01:30 +01:00
Jacob Marshall 253f35dbbb fix: update link component to allow modifier+click to properly work, and write tests (#534) 2025-02-09 18:50:45 +01:00
Jacob Marshall 0f72506f95 feat: add css key to vite config (#518) 2025-02-08 16:01:26 +01:00
Marco Pasqualetti ebe1abe422 docs: fix broken links (#532) 2025-02-07 21:02:41 +01:00
Valerio Ageno f5a7480f69 docs: add API routes page (#528)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-07 19:07:58 +01:00
Marco Pasqualetti c0139fddc8 docs(sitemap.xml): exclude 404 page (#530) 2025-02-07 19:00:43 +01:00
Valerio Ageno 9e08a949bb docs: update tuono version to v0.17.7 (#524) 2025-02-07 18:50:46 +01:00
Valerio Ageno d86c97979b chore: bump version to v0.17.7 (#523) 2025-02-07 18:35:14 +01:00
Valerio Ageno 4e266522b6 chore: update ssr_rs to v0.8.1 (#529) 2025-02-07 18:15:16 +01:00
Marco Pasqualetti a58a1a4da9 chore(deps): update dependency turbo to v2.4.0 (#525) 2025-02-07 12:07:15 +01:00
Valerio Ageno 22c1dd04c2 docs: enable posthog analytics (#519) 2025-02-06 20:58:33 +01:00
Valerio Ageno 74e34dbbf9 fix: with-tailwind template download (#522) 2025-02-06 20:55:30 +01:00
renovate[bot] 4f38de3dea chore(deps): update devdependencies (eslint) (#497)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 20:22:08 +01:00
Marco Pasqualetti 9d97f2d345 chore: refine issue templates (#508) 2025-02-06 16:21:13 +01:00
Valerio Ageno 938cf3d8c6 docs: add footer to doc (#512) 2025-02-06 15:03:39 +01:00
Valerio Ageno e1443274e3 docs: add privacy policy (#517) 2025-02-06 12:50:28 +01:00
Valerio Ageno b9f8c0312f docs: add mdx page (#514)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-06 12:48:49 +01:00
Marco Pasqualetti 674a817131 docs: add /styles/css-modules page (#513) 2025-02-06 12:41:59 +01:00
Valerio Ageno 2cbe53f7b7 docs: fix typo (#516) 2025-02-06 12:04:31 +01:00
Valerio Ageno f3033ef0d2 docs: fix quote blocks (#515) 2025-02-06 10:33:53 +01:00
Valerio Ageno 0a0fb8c7f8 docs: add Request page (#502)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-06 09:54:47 +01:00
Valerio Ageno f72a1a9e5c docs: improve readability (#510) 2025-02-05 18:33:09 +01:00
Valerio Ageno fb29a26246 docs: refine dynamic page hierarchy (#511) 2025-02-05 17:45:12 +01:00
Valerio Ageno 79d4aff758 docs: add TuonoScript component mention (#509)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-05 16:44:43 +01:00
Jacob Marshall 7ea8414d6c docs: write page for dynamic function (#503) 2025-02-05 15:01:49 +01:00
Marco Pasqualetti 1a3c9a7fbe feat: refine package.json version ranges (#507) 2025-02-05 09:28:18 +01:00
renovate[bot] cecf2ba8cc chore(deps): update dependency vitest to v2.1.9 [security] (#505)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-05 06:42:15 +01:00
Valerio Ageno eaaf2513d7 docs: add SSG page (#498)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-04 15:58:34 +01:00
Valerio Ageno fa4d435320 docs: add Response enum page (#500)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2025-02-04 15:48:26 +01:00
Marco Pasqualetti 2aff72942c docs: add getting-started/setup-project (#488) 2025-02-04 09:18:03 +01:00
renovate[bot] 11666fc422 chore(deps): update dependency happy-dom to v16.8.1 (#494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 20:01:27 +01:00
renovate[bot] aef456cbf2 chore(deps): update dependency @testing-library/react to v16.2.0 (#490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 20:00:38 +01:00
Marco Pasqualetti 97b9d71caa docs: add custom 404 page (#486) 2025-02-03 19:57:14 +01:00
renovate[bot] 5fa38b9506 chore(deps): update dependency @types/node to v22.13.0 (#492)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 18:16:22 +01:00
Marco Pasqualetti 98e50da575 docs: remove "How is tuono different?" and refine “Installation” (#480) 2025-02-03 09:44:13 +01:00
Marco Pasqualetti c8cf0771d4 test(packages/tuono-router): refine unit tests (#478) 2025-02-02 17:45:16 +01:00
Valerio Ageno 1c6e78ad57 docs: add SSR page (#473) 2025-02-02 09:39:50 +01:00
Valerio Ageno 2b6f8c16ce docs: remove valerioageno email reference (#477) 2025-02-02 09:22:16 +01:00
Jacob Marshall 2403ef3d64 docs: indent ToC based on heading levels (#475) 2025-02-02 09:04:30 +01:00
renovate[bot] 003c0766cf chore(deps): update node.js to v22.13.1 (#445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 18:00:08 +01:00
renovate[bot] 9d7e59d793 chore(deps): update dependency turbo to v2.3.4 (#430)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 17:51:22 +01:00
Valerio Ageno 43f03adedf fix: heading order (#471) 2025-01-30 21:20:34 +01:00
Valerio Ageno 211c6401e9 docs: refine UI (#466) 2025-01-30 21:04:15 +01:00
Jacob Marshall 01b90b327e docs: add headings containing only code to ToC (#467)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-30 08:48:07 +01:00
Jacob Marshall ad0b09c3df fix(docs): fix all line and link overflow issues (#426) 2025-01-29 18:42:53 +01:00
Marco Pasqualetti 362e1be58d ci: get documentation tuono version automatically (#462) 2025-01-29 17:32:35 +01:00
Marco Pasqualetti eb9039daa3 fix(crates/tuono_lib): print server url as valid URL to be clickable on CLI (#464) 2025-01-29 17:31:06 +01:00
Marco Pasqualetti ff7433f434 docs: use consistent snippet style across the codebase (#451)
Co-authored-by: wd <52734504+wdcodecn@users.noreply.github.com>
2025-01-29 08:33:55 +01:00
Marco Pasqualetti a4e207688a docs: update to tuono 0.17.6 (#461) 2025-01-29 07:20:05 +01:00
Valerio Ageno fdc87ed8f1 chore: bump version to v0.17.6 (#453) 2025-01-28 19:08:36 +01:00
Valerio Ageno 0ab9c297a5 test: integration test to tuono_lib (#425) 2025-01-28 19:08:18 +01:00
Marco Pasqualetti 04d6fcc1e1 feat(examples/tuono-tutorial): rewrite tuono-tutorial (#449) 2025-01-28 19:07:36 +01:00
Marco Pasqualetti bf18ac0ab0 docs: add notes about pull request and validating changes (#427) 2025-01-28 18:53:31 +01:00
Marco Pasqualetti bbe9605f6f docs(configuration): add documentation for server option (#446)
Co-authored-by: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com>
2025-01-28 08:53:49 +01:00
Marco Pasqualetti bd34c194cb ci: add concurrency flag for CI workflows (#448) 2025-01-28 08:30:33 +01:00
Marco Pasqualetti c0aa707315 docs(tutorial): correct api fetching example (#447) 2025-01-28 08:17:32 +01:00
Jacob Marshall b6fa448202 fix: check if project contains tuono.config.ts before running dev/build (#420) 2025-01-27 20:16:12 +01:00
Marco Pasqualetti 2e141be0fc test(packages/tuono-router): replace jsdom with happy-dom (#444) 2025-01-27 20:00:28 +01:00
Marco Pasqualetti 93b5e3e68f chore(deps): update dependency @types/react to v19.0.8 (#442) 2025-01-27 19:42:29 +01:00
spacecodeur 39dac5065f ci: add docker image test workflow (#332) 2025-01-27 19:29:08 +01:00
Marco Pasqualetti a16a313f71 ci(pr-title-checker): test type is marked as invalid (#432) 2025-01-27 01:11:11 +01:00
Jacob Marshall 82560ddc20 fix(docs): content overflow (#423) 2025-01-26 19:27:33 +01:00
Jacob Marshall 83f3cae6f0 docs: table of contents (#394)
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2025-01-26 16:36:18 +01:00
Marco Pasqualetti 99df4a5575 docs: update tuono to 0.17.5 (#416) 2025-01-26 15:02:49 +01:00
Valerio Ageno d33c211a01 chore: bump version to v0.17.5 (#417) 2025-01-25 21:16:36 +01:00
Marco Pasqualetti 21ee89ada4 fix(packages/tuono): build error when config contains vite plugins (#415) 2025-01-25 18:12:57 +01:00
Marco Pasqualetti dcedb1c6c0 refactor: create TuonoContext (#413) 2025-01-25 15:56:10 +01:00
Jacob Marshall d5cc65983e feat(crates/tuono): add --head flag to download most recent template (#408)
Co-authored-by: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com>
2025-01-25 13:42:29 +01:00
Valerio Ageno 3cb150b090 chore: bump version 0.17.4 (#409) 2025-01-23 18:57:14 +01:00
Valerio Ageno 305ae59086 feat: pass config from server to client (#407) 2025-01-23 18:43:17 +01:00
Robert Lawson 8fe8930def feat: Improve error handling (#398) 2025-01-22 20:41:59 +01:00
Valerio Ageno 8730bdbfb9 refactor: streamline readme (#403) 2025-01-22 19:07:30 +01:00
Valerio Ageno 3ee47e9481 feat: load config in tuono CLI (#399) 2025-01-22 18:01:29 +01:00
Marco Pasqualetti 4e649acd60 ci(docs): use tuono 0.17.3 in docs workflows (#404) 2025-01-22 16:07:21 +01:00
Valerio Ageno 4768d70a8a docs: remove "Components > Head" entry from sidebar (#402)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-22 13:55:19 +01:00
Marco Pasqualetti c7bff7ed09 fix(deps): update dependency vite to v5.4.12 [security] (#401) 2025-01-22 08:32:23 +01:00
Valerio Ageno 3035d2a645 feat: create tuono_internal crate (#395) 2025-01-21 19:01:37 +01:00
renovate[bot] 0750037385 fix(deps): update mantine monorepo to v7.16.1 (#388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 09:49:18 +01:00
Marco Pasqualetti 8c4d61ee65 chore(deps): update dependency @types/react to v19.0.7 (#392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 09:09:37 +01:00
Valerio Ageno 1a3ccb112e feat: add host and port support to TuonoConfig (#366)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-20 21:00:47 +01:00
Jacob Marshall 118172d27f refactor: add TuonoEntryPoint (#374)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-20 20:17:49 +01:00
renovate[bot] 69f1d85360 chore(deps): update dependency @types/node to v22.10.7 (#375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 19:58:11 +01:00
Marco Pasqualetti f0d042c85b chore(packages/tuono): remove dynamic orphan files (#385) 2025-01-20 16:30:30 +01:00
Jacob Marshall 550830c232 chore: update documentation to tuono 0.17.x (#364) 2025-01-20 16:21:35 +01:00
Valerio Ageno 6be12b4e58 chore: bump version to v0.17.3 (#372) 2025-01-19 16:24:19 +01:00
Valerio Ageno 33aa37ed7d fix: create a component tree that matches between client and server to avoid useId output mismatch (#371)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-19 16:23:30 +01:00
Marco Pasqualetti 43910fd75d ci: add unique names on ci_ok jobs (#370) 2025-01-19 11:52:57 +01:00
LeNei 5efcc86a63 fix: propagate build error and check if js files exist (#297) 2025-01-19 09:35:11 +01:00
Valerio Ageno 17b61a5a41 chore: bump to v0.17.2 (#365) 2025-01-18 20:51:28 +01:00
Jacob Marshall 5dae5c31b7 feat: add a with-tailwind example (#359)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-18 20:44:45 +01:00
Marco Pasqualetti 96deea7f31 ci: redesign CI-related workflows to allow marking them as "Required” before merging a PR (#368) 2025-01-18 20:36:06 +01:00
Jacob Marshall 7632e6a464 fix: broken docs tables (#361) 2025-01-17 15:11:57 +01:00
Jacob Marshall 23e2a2e3ff docs(styles): add Tailwind CSS (#351)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-16 18:50:56 +01:00
Jacob Marshall b19512dffc docs: move from bullet points to tables for reference docs (#352)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-16 18:42:37 +01:00
Valerio Ageno 11c6fc820b chore: bump version to v0.17.1 (#357) 2025-01-16 12:34:59 +01:00
Valerio Ageno 1a8231851d feat: add support for client only lazy loading with dynamic (#331) 2025-01-16 12:34:06 +01:00
Valerio Ageno 1dfb83b1aa fix: revert "chore: bump version to v0.17.1" (#356) 2025-01-16 12:23:46 +01:00
Valerio Ageno 93cb1ae8ba chore: bump version to v0.17.1 (#354) 2025-01-15 19:24:34 +01:00
Jacob Marshall 631b168e05 docs: more cohesive documentation for linking and navigation, Link and useRouter (#335) 2025-01-15 19:17:35 +01:00
Marco Pasqualetti af8a43c93a docs: use PascalCase for file name (#350) 2025-01-15 16:59:36 +01:00
Valerio Ageno 875d12f514 docs(configuration): remove unclear doc (#349) 2025-01-15 13:02:48 +01:00
Jacob Marshall a9a2ef67ea fix(packages/tuono): config - correct vite.plugins type (#346) 2025-01-14 21:32:51 +01:00
Marco Pasqualetti 5caf3ed6e7 chore(renovate): fix schedule (#345) 2025-01-14 21:19:14 +01:00
Marco Pasqualetti 580ec704f2 chore: dedupe deps (#347) 2025-01-14 21:12:47 +01:00
Marco Pasqualetti a9918ae555 chore(renovate): change prConcurrentLimit to 5 (default 10) and schedule once per week (#340) 2025-01-14 20:43:25 +01:00
Marco Pasqualetti 4b3f4bfab8 docs(configuration): add types and reformat vite properties (#342) 2025-01-14 20:42:54 +01:00
Jacob Marshall fed3b1581b chore: remove RouterProvider from tuono exports (#337) 2025-01-14 16:43:16 +01:00
Jacob Marshall 9b2f0aab32 chore: refine useRouter return type and update docs (#339) 2025-01-14 16:42:27 +01:00
renovate[bot] 818a6bd2ff chore(deps): update dependency typescript to v5.7.3 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 09:43:27 +01:00
renovate[bot] 94a43347a1 chore(deps): update dependency turbo to v2.3.3 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 09:14:00 +01:00
renovate[bot] 16d793c588 chore(deps): update devdependencies (eslint) (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 08:56:10 +01:00
renovate[bot] 0322e5bd8d chore(deps): update dependency postcss to v8.5.0 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 08:55:44 +01:00
renovate[bot] cfe76a9ab3 chore(deps): update dependency @types/node to v22.10.6 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 08:45:25 +01:00
renovate[bot] ff6014317b chore(deps): update pnpm to v9.15.4 (#333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 08:42:06 +01:00
PRAMOD A JINGADE 64220f2e27 docs(configuration): fill page content (#307)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-14 06:48:57 +01:00
Marco Pasqualetti e75414abfc fix(deps): revert update dependency @babel/types to v7.26.5 (#328) 2025-01-13 20:31:34 +01:00
Jacob Marshall d6fae42dc3 docs: Add useRouter Documentation (#325) 2025-01-13 18:41:31 +01:00
renovate[bot] 0db1245a6b fix(deps): update rust crate ssr_rs to 0.8.0 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 18:26:50 +01:00
renovate[bot] 91c0a9ec31 fix(deps): update dependency @babel/types to v7.26.5 (#304)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 15:44:21 +01:00
renovate[bot] 7670c37685 fix(deps): update dependency react-intersection-observer to v9.14.1 (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 15:18:24 +01:00
renovate[bot] ae41c5629c chore(deps): update rust crate serial_test to 0.10.0 (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 14:07:29 +01:00
renovate[bot] 3e7fa05fce chore(deps): update react monorepo (#301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 12:54:43 +01:00
renovate[bot] 6833640c47 chore(deps): update node.js to v22.13.0 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 12:14:33 +01:00
renovate[bot] 18541c45de chore(deps): update pnpm to v9.15.3 (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 12:14:06 +01:00
Marco Pasqualetti 45bb0dd0d2 chore: add PR labeler (#310) 2025-01-13 08:58:26 +01:00
Marco Pasqualetti db0aad2c37 chore(deps): update dependency prettier to v3.4.2 (#306) 2025-01-13 05:04:50 +01:00
Valerio Ageno d55ca12125 fix: dev script (#298) 2025-01-12 18:47:21 +01:00
Valerio Ageno ad2cada94b chore: remove benches folder (#294)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-12 13:45:20 +01:00
spacecodeur f9545f513b chore: dockerization of the tuono repo! (#276)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2025-01-12 09:54:54 +01:00
Marco Pasqualetti c75d83dc7c ci: add pr title check to enforce a consistent git history (#290) 2025-01-05 10:41:23 +01:00
Valerio Ageno 91236e5533 fix: catch-all server side handler (#287) 2025-01-04 12:07:20 +01:00
Valerio Ageno 1356be248e Revert "fix: catch-all server side handler"
This reverts commit 7e8be92cd5.
2025-01-04 10:33:52 +01:00
Valerio Ageno 7e8be92cd5 fix: catch-all server side handler 2025-01-04 10:33:17 +01:00
Valerio Ageno c3fda4a3a5 doc: update tutorial seo page 2025-01-03 17:20:46 +01:00
Valerio Ageno dfc66cfd50 doc: update tutorial 2025-01-03 17:19:09 +01:00
Marco Pasqualetti 0ef1252e1f feat(packages/tuono-(router|fs-router-vite-plugin)): replace __route with __layout (#283) 2025-01-03 17:08:18 +01:00
Valerio Ageno cf56b3a53e Load template by tag (#284) 2025-01-03 17:08:10 +01:00
Marco Pasqualetti 592f402ffd feat: add support for react 19 (#264)
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2025-01-03 11:00:44 +01:00
renovate[bot] 1dc77fc473 fix(deps): update babel monorepo (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 13:36:04 +01:00
Marco Pasqualetti 09cd772d74 chore: refine eslint ignores and fix format script of tuono-fs-router-vite-plugin (#265) 2025-01-02 13:35:22 +01:00
Marco Pasqualetti 1c69e10e6a docs: update tuono to 0.16.9 (#280) 2025-01-02 10:57:39 +01:00
Marco Pasqualetti 1af18e4c91 chore(renovate): setup a package rule for eslint (#279) 2025-01-02 10:19:28 +01:00
renovate[bot] a3709609c5 chore(deps): update dependency @testing-library/react to v16.1.0 (#275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 10:11:38 +01:00
renovate[bot] 70a79faee0 fix(deps): update dependency @types/node to v22.10.3 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:37:33 +01:00
renovate[bot] 07cb7dae9d chore(deps): update dependency vitest to v2.1.8 (#269)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:34:03 +01:00
renovate[bot] 9a1fb9c4a1 chore(deps): update dependency eslint-plugin-react to v7.37.3 (#267)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:32:48 +01:00
renovate[bot] a3f7ed7f61 fix(deps): update dependency prettier to v3.4.2 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 09:28:52 +01:00
renovate[bot] 5302fd2779 chore(deps): update react monorepo (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 08:57:01 +01:00
Marco Pasqualetti ccd4e02b6a docs(local-development): use pnpm instead of turbo (#277) 2025-01-02 08:43:41 +01:00
Valerio Ageno f6593097b6 feat: update version to v0.16.9 2024-12-24 16:20:55 +01:00
gravity 230cd22bff feat(crates/tuono): tuono new - download template by cli version and git tag (#261) 2024-12-24 16:03:29 +01:00
Valerio Ageno f4367f1755 feat: update version to v0.16.8 2024-12-24 11:57:21 +01:00
Marco Pasqualetti 7b7447f9a0 feat(packages/vite-plugins): use named export and consistent names across plugins (#260) 2024-12-24 11:41:58 +01:00
Valerio Ageno d12d8404ec feat(packages/tuono-router): replace zustand with React.Context (#256)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-24 11:41:45 +01:00
Valerio Ageno d1b12173d5 feat: update version to v0.16.7 2024-12-23 18:32:15 +01:00
Marco Pasqualetti f3bf13da59 refactor(packages/*): add tuono- prefix to all relevant folders (#255) 2024-12-23 18:28:06 +01:00
Marco Pasqualetti f236b75f55 refactor(packages/*): use @vitejs/plugin-react-swc and make all package dependencies explicit (#254) 2024-12-23 18:20:29 +01:00
Marco Pasqualetti 2434231f9b chore: add eslint-plugin-react (#251) 2024-12-23 11:43:04 +01:00
Valerio Ageno b8b39e69a1 Add message event polyfill (#250) 2024-12-22 21:35:08 +01:00
Jovan Milosevic f8158dfd8e chore(examples): Remove tsconfig.node.js file from examples (#245) 2024-12-22 20:12:19 +01:00
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
Valerio Ageno 4abc163a59 feat: update version to v0.16.2 2024-12-11 14:04:22 +01:00
Marco Pasqualetti 5bb9cf1e7c fix(packages/tuono): styles are not in the page when running tuono dev (#220) 2024-12-11 13:59:47 +01:00
Valerio Ageno a0cdf408a1 feat: update version to v0.16.1 2024-12-10 19:33:48 +01:00
Valerio Ageno 8467506915 feat: create [...catch_all] route (#210) 2024-12-10 19:30:35 +01:00
Valerio Ageno 704255774d fix: defining-routes.mdx 2024-12-10 08:22:03 +01:00
Valerio Ageno 10bb7b1cca feat: update polyfill-db json shape 2024-12-09 20:29:53 +01:00
Valerio Ageno 7f82af3d3b feat: add polyfill db to documentation (#213)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-09 12:23:33 +01:00
Marco Pasqualetti d7c729c035 docs: update tuono to 0.16 and add mdx support (#214) 2024-12-09 08:20:18 +01:00
Marco Pasqualetti 51fa904f9b docs: optimize public images (#215) 2024-12-09 07:57:25 +01:00
Valerio Ageno 6191e55745 Revert "Add ️ as default favicon for documentation and examples (#191) (#192)"
This reverts commit 7f9bf08f76.
2024-12-08 10:48:15 +01:00
Marco Pasqualetti c6e93c266a feat(packages/tuono): config – add optimizeDeps and plugins to vite options (#197) 2024-12-08 10:44:09 +01:00
Valerio Ageno bcf7e5b5ab docs: add link and navigation page content (#211)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-12-08 10:18:20 +01:00
Valerio Ageno 466f122cc4 feat: update version to v0.16.0 2024-12-08 10:13:45 +01:00
Valerio Ageno 539484ea25 docs: fill defining-routes page (#207)
Co-authored-by: Marco Pasqualetti <24919330+marcalexiei@users.noreply.github.com>
2024-12-07 14:51:57 +01:00
Valerio Ageno db1cee25bf test: add integration tests for api/ build (#208) 2024-12-07 14:17:59 +01:00
Marco Pasqualetti 96502b18d0 refactor(apps/documentation): remove lint errors (#206) 2024-12-07 12:28:57 +01:00
Valerio Ageno c491ece8c5 refactor: revamp documentation (#196)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-12-07 12:06:08 +01:00
Marco Pasqualetti c76464bf8a refactor(typescript): use generic instead of simple array type (#205) 2024-12-07 11:58:58 +01:00
Marco Pasqualetti 08b75b1a79 chore(eslint): remove build folder exclusion (#202) 2024-12-07 11:42:38 +01:00
Valerio Ageno 90da869277 feat: update version to v0.15.1 2024-12-07 11:14:13 +01:00
Valerio Ageno 80c4affaa8 feat: add cookies to the response (#201) 2024-12-07 11:10:47 +01:00
Marco Pasqualetti aa087193fe fix(crates/tuono_lib_macros): remove Cargo.tml warnings (#200) 2024-12-06 19:00:54 +01:00
Marco Pasqualetti af7e51f69b chore(ci): setup rust consistently across all workflows (#198) 2024-12-06 14:05:03 +01:00
Marco Pasqualetti 1b62d8ddc2 refactor(crates/tuono): build - ssg - disable zombie_processes warning and use TUONO_PORT to generate server URL (#199) 2024-12-06 13:05:24 +01:00
Valerio Ageno 0839579733 fix: documentation burger menu 2024-12-03 19:38:58 +01:00
Nayan Radadiya 7f9bf08f76 Add ️ as default favicon for documentation and examples (#191) (#192) 2024-12-03 19:35:25 +01:00
Valerio Ageno 79ec1c6922 feat: remove home page from documentation website (#187)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-12-03 16:29:30 +01:00
Valerio Ageno c68246df83 chore: update contributing documentation (#194) 2024-12-03 16:08:49 +01:00
Marco Pasqualetti abc0ad7120 chore(github actions): refine on paths (#189) 2024-12-03 15:59:10 +01:00
Valerio Ageno 4356ca1cff chore: update CONTRIBUTING.md 2024-12-03 14:03:41 +01:00
Valerio Ageno d6215d009c Update health_check.rs 2024-12-03 09:51:42 +01:00
Marco Pasqualetti 0b501e95a5 chore(apps/documentation): migrate to eslint 9 (#188) 2024-12-01 16:40:58 +01:00
Marco Pasqualetti f157ff37e0 refactor(packages/router): removed unused code (#184) 2024-12-01 11:03:12 +01:00
Marco Pasqualetti 11d425faa9 chore(deps-dev): update eslint to v9 (#147) 2024-12-01 10:07:35 +01:00
Valerio Ageno 5fc68a1926 feat: update documentation to v0.15.0 and fix aliases (#182)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 21:37:02 +01:00
Valerio Ageno ec4577c187 feat: add tuono.config.ts support (#153)
Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2024-11-30 19:58:04 +01:00
Valerio Ageno 8052de8ad5 feat: update version to v0.15.0 (#181) 2024-11-30 19:03:01 +01:00
Valerio Ageno 2fda959cd6 feat: update tuono new repository reference (#178)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 14:16:18 +01:00
Valerio Ageno 05fa8a5ae2 feat: update docs and readme repo links (#180) 2024-11-30 14:16:10 +01:00
Valerio Ageno a341b41e9a chore: update repo reference withing manifests (#179)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-30 14:15:36 +01:00
Valerio Ageno a2e0a4d78a feat: update version to v0.14.6 2024-11-30 14:13:21 +01:00
Valerio Ageno 4fb1d6077c Add tuono cli integration test (#177)
Co-authored-by: KingTheSim <KingTheSim@abv.bg>
Co-authored-by: KingTheSim <122887037+KingTheSim@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-29 16:34:56 +01:00
Valerio Ageno 53ab927544 feat: update version to v0.14.5 2024-11-27 21:07:50 +01:00
Valerio Ageno 897cc2daf2 feat: add --no-js-emit flag to tuono build command (#175)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-27 21:05:45 +01:00
Marco Pasqualetti 73ff31daf5 ci(rust): avoid beta and nightly toolchains to be executed on macos and windows (#174) 2024-11-27 20:40:00 +01:00
Marco Pasqualetti c387a9e17e chore(renovate): enable schedule (#172) 2024-11-27 15:33:14 +01:00
Valerio Ageno 1f92931076 Fix optional application state parameters (#173)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-27 15:25:05 +01:00
renovate[bot] d3105a1532 fix(deps): update dependency turbo to v2.3.3 (#169) 2024-11-27 08:26:54 +01:00
Marco Pasqualetti 7f50f31837 ci: trigger workflow only on push only on main (#166) 2024-11-26 21:49:14 +01:00
renovate[bot] 4c3e90ea4b chore(deps): update dependency @vitejs/plugin-react to v4.3.4 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:57 +01:00
renovate[bot] 8d80fc4aa9 chore(deps): update dependency vitest to v2.1.6 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:38 +01:00
renovate[bot] 78a04ffcf1 chore(deps): update dependency prettier to v3.4.1 (#165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 21:48:23 +01:00
Valerio Ageno bcb2c91823 fix: windows path resolution (#167) 2024-11-26 21:37:00 +01:00
Marco Pasqualetti 9058f209dd ci: add Windows and MacOS to test matrix for both rust and typescript (#158) 2024-11-26 17:50:53 +01:00
renovate[bot] 1f172d583e fix(deps): update dependency @vitejs/plugin-react-swc to v3.7.2 (#161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-26 13:48:13 +01:00
Valerio Ageno 439b4757df Turn main.rs into app.rs (#159)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-26 09:20:10 +01:00
renovate[bot] 9f8809bd51 fix(deps): update dependency @types/node to v22.10.0 (#157) 2024-11-26 08:02:27 +01:00
renovate[bot] 4e326650cc fix(deps): update dependency @types/node to v22.10.0 (#154) 2024-11-26 07:59:15 +01:00
renovate[bot] e27be27692 fix(deps): update dependency turbo to v2.3.2 (#155) 2024-11-26 07:58:58 +01:00
renovate[bot] 88eb2a1897 chore(deps): update dependency prettier to v3.4.0 (#156) 2024-11-26 07:58:43 +01:00
Valerio Ageno 8c84ef73c8 feat: update version to v0.14.1 2024-11-24 18:33:45 +01:00
renovate[bot] 859e49f16c fix(deps): update mantine monorepo to v7.14.2 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-24 18:16:13 +01:00
renovate[bot] 14ba8b0a23 chore(deps): update dependency typescript to v5.7.2 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-23 21:28:03 +01:00
Marco Pasqualetti d7c70f75f3 docs(CONTRIBUTING): add “Local development” (#136) 2024-11-23 21:04:01 +01:00
Valerio Ageno 2760e3ea08 feat(ci): update spellchecker workflow (#143) 2024-11-23 17:30:05 +01:00
Marco Pasqualetti f2f041fe55 ci: test spellcheck via reviewdog (#137)
* ci: use GITHUB_TOKEN in spell checking workflow

* ci(spell-checking): use different reporter based on github context
2024-11-23 10:56:45 +01:00
renovate[bot] 9ec9700ed4 fix(deps): update dependency @types/node to v22.9.3 (#140) 2024-11-23 10:27:51 +01:00
renovate[bot] c6b047fc02 fix(deps): update dependency @types/node to v22.9.2 (#139) 2024-11-23 09:14:22 +01:00
Myan V. 6727264e39 Better error handling when trying to overwrite an existing folder and using a non-existent template (#135)
* fix: prevent overwriting existing project directories

* refactor: improve scaffold error messages
2024-11-22 13:38:54 +01:00
Valerio Ageno d1a5fe234d fix: examples folder 2024-11-21 19:16:06 +01:00
renovate[bot] e84c7cd276 fix(deps): update dependency turbo to v2.3.1 (#132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-21 19:02:52 +01:00
Valerio Ageno 087813fab9 Add support for api/ folder (#129)
* feat: detect api/ folder from tuono CLI

* feat: prevent adding the API in the data endpoints list

* feat: create basic API handler proc macro

* feat: parse api/ and build .tuono/main.rs file

* chore: remove unused cargo dependencies

* refactor: tuono base health_check

* fix: remove failing test

* feat: update version to v0.14.0
2024-11-21 19:02:11 +01:00
Valerio Ageno 3a4e7398c6 feat: update documentation og:cover 2024-11-21 18:53:32 +01:00
Valerio Ageno 1630fd7e71 Update README.md 2024-11-21 11:30:02 +01:00
Myan V. 4aa99beaff Better error handling for target ports conflict (#126)
* feat: add error message when port 3000 is already in use

* feat: expand port availability checking

* refactor: improve port extraction code modularity

* fix: formatting

* fix: clippy

* fix: move port checking logic to tuono crate

* fix(tuono): make main async

* fix: tokio panicking when testing CLI

* feat: working port conflict detection in crate tuono

* fix: passing format and lint checks

* refactor: refactor cli by making it non-async

* refactor: move port constants to module scope and remove unnecessary Result return type from port checker
2024-11-21 09:45:08 +01:00
Valerio Ageno eaa0ba28d8 Update README.md 2024-11-20 18:57:43 +01:00
renovate[bot] 3f3e1120b2 chore(deps): update pnpm to v9.14.2 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 13:52:19 +01:00
Marco Pasqualetti 89d7a2c740 chore: add examples folder to workspaces and make examples packages name unique (#125) 2024-11-20 13:46:31 +01:00
Valerio Ageno 56c31f0204 feat: update version to v0.13.3 2024-11-20 13:45:37 +01:00
renovate[bot] 338cfb5be2 fix(deps): update dependency @types/node to v22.9.1 (#121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:32 +01:00
renovate[bot] 43f6fe664b chore(deps): update pnpm to v9.14.1 (#124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-20 09:03:03 +01:00
Valerio Ageno 209c9fb7f7 feat: update version to v0.13.2 2024-11-19 17:34:33 +01:00
Valerio Ageno a562f67e8d Feat: added discord to README 2024-11-19 16:43:58 +01:00
Valerio Ageno ef0fe5a425 fix: seo page canonical 2024-11-19 15:50:58 +01:00
Myan V. b50902d23e Update watchexec to v5 and watchexec-signals to v4 (#118)
* fix(deps): update rust crate watchexec to v5

* fix(deps): update rust crate watchexec-signals to v4

* fix: revert unnecessary imports
2024-11-19 14:32:44 +01:00
Marco Pasqualetti 55490579ec refactor: replace () => JSX.Element with React.ComponentType (#120) 2024-11-18 21:45:27 +01:00
renovate[bot] 957d08701f chore(deps): update typescript-eslint monorepo to v8.15.0 (#119)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 20:41:04 +01:00
Valerio Ageno 61e1aed35d Add not expiring discord invite to documentation website 2024-11-18 18:36:33 +01:00
Valerio Ageno 2034f0ef5f fix: tutorial/redirections canonical tag 2024-11-18 12:35:14 +01:00
Valerio Ageno fcb92c7f0b fix(doc): spelling 2024-11-17 18:24:56 +01:00
Marco Pasqualetti 28ea1ca1df ci: add format check on repository root (#115) 2024-11-17 17:51:20 +01:00
Valerio Ageno ebc1a458a9 Fix tuono_lib_macro crate (#116)
* fix: tuono lib macro

* feat: remove reqwest re-export

* feat: update version to 0.13.1

* fix: remove reqwest from documentation
2024-11-17 17:11:07 +01:00
Valerio Ageno df92ef5296 Add support for custom global state (#111)
* feat: add 'has_main_file' to App struct

* feat: handle custom state from the main.rs file

* feat: update tutorial template with new setup

* fix: apply clippy hints

* feat: add ApplicationState mention to documentation website

* feat: update version to v0.13.0
2024-11-17 16:28:56 +01:00
Valerio Ageno 253e0a8378 feat(doc): keep social buttons on mobile website 2024-11-17 12:26:58 +01:00
Valerio Ageno a9fbe4eddd feat: add discord channel invite to documentation website 2024-11-17 12:19:25 +01:00
Valerio Ageno cfccc638c6 feat: update version to v0.12.4 2024-11-16 19:23:21 +01:00
renovate[bot] 9f1460cc26 fix(deps): update mantine monorepo to v7.14.1 (#110) 2024-11-16 17:11:02 +01:00
Henry Chen f0a4541420 fix: handle hyphen in route (#109) 2024-11-16 17:10:37 +01:00
renovate[bot] d0ce482680 chore(deps): update dependency vitest to v2.1.5 (#105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:29:50 +01:00
renovate[bot] 40e2734a4e fix(deps): update dependency turbo to v2.3.0 (#106)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-16 14:28:58 +01:00
Marco Pasqualetti 8552796343 chore: fix typo and improve name on CI (#107) 2024-11-16 11:47:53 +01:00
Marco Pasqualetti 6d72ffb876 ci: create install-node-dependencies action (#100) 2024-11-16 11:27:10 +01:00
Valerio Ageno 65faaf0183 feat: added website to crates/package manifest 2024-11-16 11:08:50 +01:00
Valerio Ageno 7f7c2e8d6f feat: update documentation tuono to v0.12.3 (#104) 2024-11-16 11:02:47 +01:00
Valerio Ageno 3be491baa7 Fix tuono lib release (#103)
* fix: removed service

* feat: update version to v0.12.3
2024-11-16 10:50:02 +01:00
Valerio Ageno 48b974ae58 Fix failing release pipeline (#102)
* feat: add no-verify key to cargo publish script

* feat: update version to v0.12.2
2024-11-16 10:23:59 +01:00
Valerio Ageno c394898fcd Create logger service (#101)
* feat: create logger service

* feat: opt out __tuono/data routes from logger

* feat: update version to v0.12.1
2024-11-16 09:47:59 +01:00
renovate[bot] f22249f3e9 chore(deps): update pnpm to v9.13.2 (#99) 2024-11-15 07:08:49 +01:00
renovate[bot] 120cec52e9 chore(deps): update pnpm to v9.13.1 (#98)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 20:53:19 +01:00
Marco Pasqualetti 535cc6b3d9 chore: add apps/documentation to pnpm workspaces (#96) 2024-11-14 10:30:21 +01:00
renovate[bot] 18a060e2bf chore(deps): update pnpm to v9.13.0 (#94)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-13 16:55:37 +01:00
Marco Pasqualetti 7b8165cee6 refactor: use React.JSX rather than global JSX (#93) 2024-11-13 08:33:48 +01:00
Valerio Ageno 6331eb64e6 fix: development script watch loop (#92)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-12 14:21:16 +01:00
Marco Pasqualetti 3c38b551f0 docs(CONTRIBUTING): new link issue point to issue template chooser (#90) 2024-11-12 13:46:34 +01:00
renovate[bot] 8e761347f2 fix(deps): update rust crate tuono_lib to 0.12.0 (#89)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-12 08:45:21 +01:00
Valerio Ageno e2d44e0e6a fix: format documentation 2024-11-11 21:56:22 +01:00
Valerio Ageno 607da92c71 Make handler arguments optional (#87)
* feat: support axum extractors

* chore: update tuono example

* chore: update tutorial example

* doc: update documentation

* feat: update version to v0.12.0

* fix: use clippy suggestion
2024-11-11 21:52:22 +01:00
Henry Chen ae0427604e Fix tutorial link in README (#86) 2024-11-11 13:32:56 +01:00
Marco Pasqualetti 2aa981c581 chore: use GitHub forms in issue template (#85)
* chore: use GitHub forms in issue template

* Update .github/ISSUE_TEMPLATE/bug_report.yml

---------

Co-authored-by: Valerio Ageno <51341197+Valerioageno@users.noreply.github.com>
2024-11-11 09:28:25 +01:00
Valerio Ageno 5d8ec82502 feat: add log to catch_all routes (#83) 2024-11-10 11:08:06 +01:00
Valerio Ageno fc9bb52c41 Update getting started (#82)
* feat: update getting started doc

* feat: update README
2024-11-10 10:44:36 +01:00
Valerio Ageno 3224e4138f feat: update doc hero 2024-11-10 09:52:51 +01:00
Valerio Ageno 7ea28c4fd3 fix: documentation typo 2024-11-10 09:50:47 +01:00
Valerio Ageno acb5567f6a fix: breadcrumbs structured data 2024-11-09 19:27:02 +01:00
Valerio Ageno cc877c7c11 fix: doc formatting 2024-11-06 21:11:57 +01:00
Valerio Ageno 87bdddf7cf feat: add twitter OG 2024-11-06 21:11:32 +01:00
Valerio Ageno d939b64d64 fix: documentation meta breadcrumb 2024-11-06 21:09:30 +01:00
Valerio Ageno 3f64f71947 feat: update documentation version to v0.11.2 (#81)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-05 12:13:30 +01:00
Valerio Ageno aaae7db24d Improve route preloading (#80)
* feat: improve route preloading

* feat: update version to v0.11.2

---------

Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-11-05 12:03:20 +01:00
Valerio Ageno 48c3934b6e Add seo tags to documentation (#78)
* refactor: update documentation meta tags

* feat: update documentation version to v0.11.1

* fix: formatting

* feat: add SEO breadcrumb structured data
2024-11-04 21:23:56 +01:00
Valerio Ageno 258b4af0b1 feat: prevent scroll offset on page navigation (#77) 2024-11-04 19:29:37 +01:00
Valerio Ageno edfbd03f13 Prevent not exported routes error (#76)
* fix: prevent no exported route compiler error

* feat: update version to v0.11.1

* fix: formatting
2024-11-04 19:25:46 +01:00
Valerio Ageno 9cb0f62da7 fix: documentation server/client mismatch 2024-11-04 17:15:37 +01:00
Valerio Ageno 9591960e31 feat: add canonical tags to documentation 2024-11-03 22:34:23 +01:00
Valerio Ageno 132d8ebe8f feat: add navigation buttons to tutorial 2024-11-03 22:34:12 +01:00
Valerio Ageno 4020073aca feat: update doc tuono version to v0.11.0 2024-11-03 22:34:00 +01:00
Valerio Ageno 5025692220 fix: put mantine color scheme component outside Head 2024-11-03 22:27:17 +01:00
Valerio Ageno d04a4c4103 Move to react-helmet-async (#72)
* feat: move to react-helmet-async

* feat: set version to v0.11.0
2024-11-03 21:09:32 +01:00
renovate[bot] 7e6dea68f4 chore(deps): update pnpm to v9.12.3 (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 19:02:59 +01:00
renovate[bot] a046833414 chore(deps): update dependency node to v22 (#71)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 19:02:43 +01:00
renovate[bot] cfa59ca8bf fix(deps): update dependency @types/node to v22 (#70) 2024-10-29 07:02:39 +01:00
renovate[bot] d470bd58f2 doc: update tuono version to v0.10.4 (#61)
Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-10-18 15:22:54 +02:00
Valerio Ageno ea82d2b1df Correct documentation spelling (#68)
* fix: documentation/index spelling

* fix: documentation/development-setup spelling

* fix: documentation/api-fetching spelling

* fix: documentation/components spelling

* fix: documentation/dynamic-routes spelling

* feat: add correct tutorial link to README.md

* chore: remove legacy  folder

* fix: formatting

* fix: documentation/error-handling spelling

* fix: documentation/seo spelling

* fix: documentation/redirections spelling

* fix: documentation/production spelling

* fix: documentation/conclusion spelling

* feat: update version to v0.10.4

* fix: redirections spelling

* fix: API fetching spelling

* feat: create CLI doc page

* fix: correct CLI spelling

* fix: update CLI output spelling
2024-10-17 19:00:23 +02:00
renovate[bot] 93b44088a9 chore(deps): update dependency node to v20.18.0 (#60)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 18:09:15 +02:00
renovate[bot] 625c091fdc chore(deps): update dependency @testing-library/react to v16 (#63)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 18:08:57 +02:00
Valerio Ageno e0c0447fb6 fix: remove useless container 2024-10-17 12:17:38 +02:00
Valerio Ageno b67a8e8537 fix: remove table of contents 2024-10-17 12:14:33 +02:00
renovate[bot] c9f1b02954 chore(deps): update dependency jsdom to v25 (#64)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 09:47:47 +02:00
renovate[bot] 6b3bfca1f6 chore(deps): update dependency vitest to v2 (#65)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 09:47:29 +02:00
Valerio Ageno 15e568594d feat: update code font size 2024-10-17 09:03:05 +02:00
Valerio Ageno 089f41ee89 feat: update doc code example font size 2024-10-17 08:56:17 +02:00
Valerio Ageno 636c68e532 chore: update doc sidebar 2024-10-17 08:55:23 +02:00
Valerio Ageno 6d8a04dfb2 Move tutorial into web documentation (#54)
* chore: move tutorial into web documentation

* fix: update ci-documentation tuono version

* fix: rollback documentation website version to v0.10.1

* temporaly remove 'dynamic' keyword

* feat: update tuono documentation to v0.10.3

* chore: format

---------

Co-authored-by: Valerio Ageno <valerio.ageno@qonto.com>
2024-10-17 08:20:01 +02:00
Valerio Ageno 01d5aad8aa fix: update testing-library/jest-dom version 2024-10-16 20:49:46 +02:00
Valerio Ageno 002733cd99 Prevent dynamic fn replacement when not imported by tuono (#57)
* fix: prevent dynamic fn replacement when not imported by tuono

* feat: update version to v0.10.3

* Revert "fix(deps): update dependency zustand to v5 (#55)"

This reverts commit d7a086dfb1.

* Revert "fix(deps): update dependency zustand to v4.5.5 (#37)"

This reverts commit b36dc0b897.

* Revert "chore(deps): update dependency @testing-library/react to v16 (#38)"

This reverts commit 1a1b62698c.

* Revert "chore(deps): update dependency @tanstack/config to ^0.13.0 (#32)"

This reverts commit 6c7cc62371.

* revert pnpm update

* Revert "chore(deps): update dependency vitest to v2 (#45)"

This reverts commit c5b5f6aefe.

* Revert "chore(deps): update dependency node to v20.18.0 (#33)"

This reverts commit 588326cc57.
2024-10-16 20:41:33 +02:00
renovate[bot] d7a086dfb1 fix(deps): update dependency zustand to v5 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 19:38:29 +02:00
renovate[bot] d60c7dcd9b fix(deps): update rust crate watchexec-supervisor to v3 (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 19:29:33 +02:00
renovate[bot] 58baf8aa82 fix(deps): update dependency tuono to v0.10.2 (#51)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 17:57:44 +02:00
Valerio Ageno 0a8504d9f2 feat: update version to v0.10.2 (#50) 2024-10-13 10:18:19 +02:00
Valerio Ageno 934a83c0e2 Fix spelling (#49)
* fix: README spelling

* fix: CONTRIBUTING spelling

* fix: CODE_OF_CONDUCT

* fix: documentation spelling
2024-10-12 19:03:05 +02:00
Valerio Ageno ea65ad042f fix: documentation eslint 2024-10-12 19:02:17 +02:00
renovate[bot] 3544cd2943 chore(deps): update typescript-eslint monorepo to v8 (major) (#47)
* chore(deps): update typescript-eslint monorepo to v8

* chore: update ban-type eslint rule

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 18:40:04 +02:00
renovate[bot] e593707ce7 fix(deps): update rust crate syn to v2 (#48)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 18:38:44 +02:00
renovate[bot] 6dd0fc7410 chore(deps): update actions/checkout action to v4 (#46) 2024-10-12 13:55:40 +02:00
renovate[bot] faf7bf29e2 fix(deps): update rust crate tungstenite to 0.24.0 (#44) 2024-10-12 13:41:03 +02:00
renovate[bot] c5b5f6aefe chore(deps): update dependency vitest to v2 (#45) 2024-10-12 13:40:51 +02:00
renovate[bot] 8e48eec01d chore(deps): update dependency eslint-plugin-react-hooks to v5 (#42) 2024-10-12 13:37:33 +02:00
renovate[bot] e3c5cc882f chore(deps): update dependency jsdom to v25 (#43) 2024-10-12 13:37:15 +02:00
renovate[bot] dfb1bebc6b fix(deps): update rust crate tokio-tungstenite to 0.24.0 (#39)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:03:59 +02:00
renovate[bot] a40367f813 fix(deps): update rust crate tower-http to 0.6.0 (#40)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:03:44 +02:00
renovate[bot] 1a1b62698c chore(deps): update dependency @testing-library/react to v16 (#38)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 13:00:23 +02:00
renovate[bot] b36dc0b897 fix(deps): update dependency zustand to v4.5.5 (#37)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:55:01 +02:00
renovate[bot] a9eacf21f4 chore(deps): update pnpm to v9.12.1 (#36)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:51:50 +02:00
renovate[bot] 23d84b25be fix(deps): update rust crate ssr_rs to 0.7.0 (#35)
* fix(deps): update rust crate ssr_rs to 0.7.0

* chore: update ssr error

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:45:23 +02:00
renovate[bot] 6c7cc62371 chore(deps): update dependency @tanstack/config to ^0.13.0 (#32)
* chore(deps): update dependency @tanstack/config to ^0.13.0

* fix: tanstack/config vite import

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:40:03 +02:00
renovate[bot] 588326cc57 chore(deps): update dependency node to v20.18.0 (#33)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-12 12:34:06 +02:00
renovate[bot] ce654bf7a2 chore: Configure Renovate (#31)
* Add renovate.json

* fix: ignore benches/

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valerio Ageno <valerioageno@yahoo.it>
2024-10-12 12:29:14 +02:00
Valerio Ageno a3bcb76105 CI: add spell checking pipeline (#30)
* ci: add spell checking pipeline

* fix: add github to the lookup folders

* fix: add patterns rule

* refactor: Github action definition

* test: introduce spell bug

* fix: update spell checker reporter

* fix: update action version

* feat: remove disabled_categories

* feat: remove github token

* feat: add fail level to pipeline

* fix: error as string

* feat: remove disabled rules

* feat: add reviewdog_token

* feat: change disabled_rules to string

* feat: add spell rules

* fix: spell categories

* Update bug_report.md
2024-10-12 12:05:45 +02:00
Valerio Ageno 4045ebc24e Update README.md 2024-08-20 14:34:37 +02:00
Valerio Ageno 2be3710462 Update README.md 2024-08-20 14:30:46 +02:00
Valerio Ageno fe3185c6dc Update documentation version to v0.10.1 2024-08-19 21:03:50 +02:00
Valerio Ageno f10b813e1e Update ci-documentation.yml 2024-08-19 18:44:32 +02:00
Valerio Ageno c67d4199a0 fix: github language checks 2024-08-19 18:27:45 +02:00
Valerio Ageno d0a6c59c62 Add response time to SSR GET log (#29)
* feat: add response time to SSR GET log

* feat: update version to v0.10.1
2024-08-19 18:25:00 +02:00
Valerio Ageno 60f622542f feat: update tuono version in deploy documentation CD 2024-08-18 12:36:51 +02:00
Valerio Ageno e7fcd64b3a feat: add sitemaps to documentation website 2024-08-18 12:25:12 +02:00
Valerio Ageno 3772e7307e feat: add favicon to documentation website 2024-08-18 11:32:57 +02:00
Valerio Ageno e853db164d fix: documentation types CI 2024-08-18 11:25:14 +02:00
Valerio Ageno 509283adab feat: update tuono version to v0.10.0 2024-08-18 11:19:54 +02:00
Valerio Ageno db56b7f2dc feat: remove default head meta tags (#28) 2024-08-18 11:17:38 +02:00
Valerio Ageno acd663d79f Add custom response type (#27)
* feat: support route names with dots

* feat: add support for Response::Custom value

* feat: statically export xml files
2024-08-18 11:09:27 +02:00
Valerio Ageno 3636268a71 fix: documentation hero 2024-08-17 20:16:26 +02:00
Valerio Ageno 6402f42d01 fix: documentation mdx container 2024-08-17 20:08:23 +02:00
Valerio Ageno 71ac19cbb9 fix: documentation padding 2024-08-17 17:35:45 +02:00
Valerio Ageno d99389a398 fix: prod TOC 2024-08-17 17:32:25 +02:00
Valerio Ageno 024c386af4 feat: add table of content 2024-08-17 17:21:29 +02:00
Valerio Ageno d982c9c24b feat: change documenation border radious 2024-08-17 16:23:57 +02:00
Valerio Ageno 480973318d feat: add X button on documentation website 2024-08-17 15:37:21 +02:00
Valerio Ageno a497937106 feat: update benches to speed up nextjs servers 2024-08-17 15:00:43 +02:00
Valerio Ageno 0d33150f30 feat: add benchmarks against nextjs (#26) 2024-08-16 16:23:39 +02:00
Valerio Ageno 94667b1454 Update documentation tuono version to v0.9.2 2024-08-16 11:38:24 +02:00
Valerio Ageno 2cbcd8ad70 feat: update version to v0.9.2 2024-08-16 11:28:42 +02:00
Valerio Ageno 9f40a55907 feat: add id to documentation headings 2024-08-16 11:27:29 +02:00
Valerio Ageno 51e01ec142 feat: add link support to hash 2024-08-16 11:21:18 +02:00
Valerio Ageno 63fc2b9855 fix: documentation mailto link 2024-08-16 11:05:49 +02:00
Valerio Ageno edd8a02423 fix: documentation type checking 2024-08-15 21:16:38 +02:00
Valerio Ageno a0a81b514c fix: documentation formatting 2024-08-15 21:08:48 +02:00
Valerio Ageno 92820804f8 fix: sidebar on mobile 2024-08-15 21:08:18 +02:00
Valerio Ageno e7ea7ed1c4 feat: improve contributing documentation 2024-08-15 21:07:28 +02:00
Valerio Ageno 7471c90f49 feat: update documentation tuono version to v0.9.1 2024-08-15 20:52:22 +02:00
Valerio Ageno f545a9c469 feat: update contributing and tutorial pages 2024-08-15 20:51:58 +02:00
Valerio Ageno 30073c226d feat: update version to v0.9.1 2024-08-15 20:44:25 +02:00
Valerio Ageno 1b1e91b6c0 fix: sanitize client side pathnames (#25) 2024-08-15 20:42:47 +02:00
Valerio Ageno 71d7ccf27d feat: update README header 2024-08-15 20:36:11 +02:00
Valerio Ageno 3ca24ec87f feat: create overview and getting started pages 2024-08-15 19:25:03 +02:00
Valerio Ageno b2940d9fa8 feat: add breadcrumbs to documentation 2024-08-15 18:52:56 +02:00
Valerio Ageno a7b7f0eac2 feat: update documentation sidebar 2024-08-15 18:20:20 +02:00
Valerio Ageno 4859916edd feat: update documentation sidebar 2024-08-15 11:31:33 +02:00
Valerio Ageno ad3e19e965 fix: documentation sidebar on mobile 2024-08-14 21:28:23 +02:00
Valerio Ageno 03dead1083 feat: update documentation tuono version to v0.9.0 2024-08-14 20:59:43 +02:00
Valerio Ageno 341fba6ca9 Preload chunks when links are visible in the viewport (#24)
* feat: preload chunk when links are visible in the viewport

* feat: update version to v0.9.0
2024-08-14 20:50:47 +02:00
Valerio Ageno 58c4890122 Update pull_request_template.md 2024-08-14 20:45:37 +02:00
Valerio Ageno af42e5ef21 fix: remove useless pendingElement 2024-08-14 14:48:26 +02:00
Valerio Ageno 5abff0384c feat: update documentation navbar 2024-08-14 14:45:47 +02:00
Valerio Ageno cc915560b8 update documentation tuono version to v0.8.4 2024-08-14 14:41:22 +02:00
Valerio Ageno 9835a4c0d1 fix: route switch wrappers re-rendering (#23)
* fix: route switch wrappers re-rendering

* feat: update version to v0.8.4
2024-08-14 14:26:52 +02:00
Valerio Ageno 90782ae710 fix: turbo dev task config 2024-08-11 18:19:38 +02:00
Valerio Ageno 75c790dd6e refactor: create standalone router package (#22)
* refactor: create standalone router package

* fix: add jsdom to tuono-router package

* fix: remove test in tuono vite.config

* update turbo to latest

* fix: wait build before lint

* fix: router types
2024-08-11 17:14:54 +02:00
Valerio Ageno 8604858a61 fix: documentation formatting 2024-08-11 16:33:45 +02:00
Valerio Ageno d1b6162d3a Update contributing.mdx 2024-08-09 17:48:23 +02:00
Valerio Ageno a445644b47 Update contributing.mdx 2024-08-09 17:47:07 +02:00
Valerio Ageno ebb4434506 Update contributing.mdx 2024-08-09 17:40:42 +02:00
Valerio Ageno 36a53662a0 Update contributing.mdx 2024-08-09 17:40:18 +02:00
Valerio Ageno 569d95dc9c fix: routing page 2024-07-27 14:58:18 +02:00
Valerio Ageno bc4c4e03f0 feat: update documentation tuono version (#21) 2024-07-27 14:52:02 +02:00
Valerio Ageno 170c5ea098 Update static build html structures (#20)
* fix: improve static build output structure

* feat: update version to v0.8.3
2024-07-27 14:42:28 +02:00
Valerio Ageno 3fd5f166e2 fix: edit page github link 2024-07-27 11:40:24 +02:00
Valerio Ageno a2285a78a1 fix: documentation output folder 2024-07-27 11:38:18 +02:00
Valerio Ageno 1ae7f72de3 Create documentation website (#19)
* feat: scaffold documentation app

* feat: init documentation website with mantine

* feat: add theme button

* feat: setup mdx link component

* feat: setup basic documentation folder

* feat: add routing folder

* feat: scaffold homepage

* feat: refined navbar and hero

* feat: create pre mdx component

* feat: create mdx blockquote component

* feat: create code mdx component

* fix: remove HTMLProps type

* feat: create base hero page

* ci: add lint/formatting/type checking to documentation

* ci: add documentation pipeline

* fix: static build documentation

* fix: remove documentation test check

* fix: install deps outside workspace

* fix: filter prettier checks

* fix: eslint plugins

* fix: clone eslintrc

* feat: update dark theme colors

* doc: add installation page

* feat: add deploy documenation CD pipeline

* fix: update documentation bucket region

* feat: update documentation head tags

* fix: deploy website on main branch push
2024-07-27 11:33:34 +02:00
313 changed files with 13310 additions and 3229 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
-103
View File
@@ -1,103 +0,0 @@
{
"root": true,
"reportUnusedDisableDirectives": true,
"ignorePatterns": ["**/build", "**/dist"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
],
"env": {
"browser": true,
"es2020": true,
},
"parserOptions": {
"tsconfigRootDir": ".",
"project": true,
"sourceType": "module",
"ecmaVersion": 2020,
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": true,
},
"react": {
"version": "detect",
},
},
"rules": {
"@typescript-eslint/array-type": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/ban-ts-comment": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{ "prefer": "type-imports" },
],
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/method-signature-style": ["error", "property"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "typeParameter",
"format": ["PascalCase"],
"leadingUnderscore": "forbid",
"trailingUnderscore": "forbid",
"custom": {
"regex": "^(T|T[A-Z][A-Za-z]+)$",
"match": true,
},
},
],
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-inferrable-types": [
"error",
{ "ignoreParameters": true },
],
"import/default": "error",
"import/export": "error",
"import/namespace": "error",
"import/newline-after-import": "error",
"import/no-cycle": "error",
"import/no-duplicates": "off",
"import/no-named-as-default-member": "error",
"import/no-unused-modules": "error",
"import/order": [
"off",
{
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
],
},
],
"no-case-declarations": "error",
"no-empty": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-shadow": "error",
"no-undef": "off",
"sort-imports": "off",
},
}
+9
View File
@@ -1 +1,10 @@
examples/** linguist-documentation
*.css linguist-detectable=false
*.scss linguist-detectable=false
*.js linguist-detectable=false
*.jsx linguist-detectable=true
*.tsx linguist-detectable=true
*.ts linguist-detectable=true
*.rs linguist-detectable=true
*.html linguist-detectable=false
*.sh linguist-detectable=false
-23
View File
@@ -1,23 +0,0 @@
## Description
A clear and concise description of what the bug is.
## Expected behaviour
A clear and concise description of what you expected to happen.
## How to reproduce
Steps to reproduce the behavior:
## Screenshots
If applicable, add screenshots to help explain your problem.
## Tooling and environment
- Tuono version [e.g. 0.4.0]
- OS: [e.g. MacOS, Windows]
- Browser: [e.g. chrome, safari]
- Node version: [e.g. 20.0.0]
- Rust version: [e.g. 1.79.0]
- Crate version: [e.g. 1.78.0]
- Node Package Manger version: [e.g. pnpm: 9.5.0]
## Additional context
Add any other context about the problem here.
+60
View File
@@ -0,0 +1,60 @@
name: Bug report
description: File a bug report
title: '[Bug]: '
type: Bug
labels: ['bug']
body:
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: expected-behaviour
attributes:
label: Expected behaviour
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: steps
attributes:
label: How to reproduce
description: Steps to reproduce the behavior (code, example repository)
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --npmPackages 'tuono' --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
validations:
required: true
- type: textarea
id: system-info-rust
attributes:
label: System info (Rust)
description: Output of `rustc --version && cargo --version && tuono --version`
render: shell
placeholder: 'rustc, cargo and tuono binary version'
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the bug here.
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Ask a question / Get support
url: https://discord.com/invite/khQzPa654B
about: Ask questions and discuss with other community members
-20
View File
@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: '[Feature request]: '
type: Feature
labels: ['enhancement']
body:
- type: textarea
id: description
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is.
placeholder: I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false
@@ -0,0 +1,37 @@
name: Install NodeJS Dependencies
description: This is a composite GitHub Action that sets up pnpm, node and installs the project's dependencies.
inputs:
node-version:
description: 'Explicit node version. Otherwise fallback reading `.nvmrc`. Use in conjunction with matrix'
required: false
registry-url:
description: 'https://github.com/actions/setup-node?tab=readme-ov-file#usage'
required: false
default: ''
runs:
using: composite
steps:
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js (via input)
if: ${{ inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: ${{ inputs.registry-url }}
cache: 'pnpm'
- name: Setup Node.js (via .nvmrc)
if: ${{ !inputs.node-version }}
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
registry-url: ${{ inputs.registry-url }}
cache: 'pnpm'
- name: Install Dependencies
shell: bash
run: pnpm install --frozen-lockfile
+32
View File
@@ -0,0 +1,32 @@
# @see https://github.com/actions/labeler
rust:
- changed-files:
- any-glob-to-any-file: ['crates/**', 'Cargo.toml']
typescript:
- changed-files:
- any-glob-to-any-file:
[
'packages/**',
'package.json',
'pnpm-*.yaml',
'eslint.config.js',
'tsconfig.json',
]
'CI/CD':
- changed-files:
- any-glob-to-any-file: ['.github/**']
'repo maintenance':
- changed-files:
- any-glob-to-any-file:
[
'.npmrc',
'.nvmrc',
'.prettierrc',
'.prettierignore',
'renovate.json',
'turbo.json',
]
+16 -10
View File
@@ -1,18 +1,24 @@
## Context
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? If a new feature is being added, describe the intended
use case that feature fulfills.
👋 Thank you for your Pull Request 🙏
-->
## Description
### Checklist
- [ ] I have read [Contributing > Pull requests](https://tuono.dev/documentation/contributing/pull-requests)
### Related issue
Fixes #0
<!-- Replace the content with "None" if you haven't an issue to link -->
### Overview
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.
Bug fixes and new features should include tests.
Explain the context and why you're making that change.
What is the problem you're trying to solve?
If a new feature is being added,
describe the intended use case that feature fulfills.
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
-->
+61
View File
@@ -0,0 +1,61 @@
name: Docker CI
# If we need support more OS refer to
# https://github.com/tuono-labs/tuono/pull/332#issuecomment-2614439692
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build_and_test:
name: Build and Test Docker Image on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build and Run Docker Compose
run: |
docker compose -f docker/compose.yml up --build -d
- name: Check if the container is running
run: |
docker ps --filter "name=tuono-source-container" --format "table {{.Names}}\t{{.Status}}"
if ! docker ps --filter "name=tuono-source-container" --format "{{.Status}}" | grep -q "Up"; then
echo "Error: Container 'tuono-source-container' is not running."
exit 1
fi
- name: Check commands inside container
run: |
docker exec tuono-source-container pnpm --version || { echo "Error: pnpm is not available."; exit 1; }
docker exec tuono-source-container tuono --version || { echo "Error: tuono is not available."; exit 1; }
- name: Run cargo test
run: docker exec tuono-source-container cargo test
- name: Install node dependencies in each tuono's sub-projets
run: docker exec tuono-source-container bash -c 'for dir in /tuono/packages/*; do [ -d "$dir" ] && (cd "$dir" && yes | pnpm install --frozen-lockfile); done'
- name: Run pnpm test
run: docker exec tuono-source-container pnpm test
- name: Tear Down Docker Compose
if: always()
run: |
docker compose -f docker/compose.yml down --volumes
+61
View File
@@ -0,0 +1,61 @@
name: E2E CI
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
e2e:
name: E2E Test (${{ matrix.os }})
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Setup rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install Playwright Browsers
run: pnpm exec playwright install
- name: Run Playwright E2E tests
run: pnpm test:e2e
ci_ok:
name: E2E CI OK
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [e2e]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
+27
View File
@@ -0,0 +1,27 @@
name: 'PR Labeler'
# @see https://github.com/actions/labeler
# @see .github/labeler.yml
on:
- pull_request_target
jobs:
labeler:
name: 'Manage labels'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
# Add checkout step to load labeler.yml locally,
# rather than from an API request
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/labeler@v5
with:
sync-labels: true
+61
View File
@@ -0,0 +1,61 @@
name: 'PR Title Checker'
# This workflow ensures that PR titles adhere to a pattern
# similar to conventional commits standard
on:
pull_request:
types:
- opened
- edited
- synchronize
- labeled
- unlabeled
env:
PR_TITLE: ${{ github.event.pull_request.title }}
jobs:
check_pr_title:
name: 'PR Title Checker'
runs-on: ubuntu-latest
steps:
- name: Check type
id: type
# @warning Keep in sync with https://github.com/tuono-labs/tuono-documentation/blob/main/src/routes/documentation/contributing/pull-requests.mdx
run: |
VALID_COMMIT_TYPES="chore|ci|docs|feat|fix|refactor|test"
REGEX="^(${VALID_COMMIT_TYPES})(\(.*\))?!?: .*"
if ! [[ $PR_TITLE =~ $REGEX ]]; then
echo "::error title=Type::The title has an incorrect type. Valid types are ${VALID_COMMIT_TYPES}"
echo "result=fail" >> "$GITHUB_OUTPUT"
else
echo "::notice title=Type::The title is using a valid type"
echo "result=ok" >> "$GITHUB_OUTPUT"
fi
- name: Check length
id: length
run: |
MAX_LENGTH=100
LENGTH=${#PR_TITLE}
if [ $LENGTH -gt $MAX_LENGTH ]; then
echo "::error title=Length::The title is longer than $MAX_LENGTH characters"
echo "result=fail" >> "$GITHUB_OUTPUT"
else
echo "::notice title=Length::The title length is within the maximum value of $MAX_LENGTH characters"
echo "result=ok" >> "$GITHUB_OUTPUT"
fi
- name: Check result
env:
FAILURE: ${{ contains(join(steps.*.outputs.result, ','), 'fail') }}
run: |
echo "Failure: $FAILURE"
if [ "$FAILURE" = "false" ]; then
exit 0
else
exit 1
fi
+42 -47
View File
@@ -1,78 +1,68 @@
name: Publish on npm and crates
on:
release:
types: [published]
jobs:
publish-on-npm-dry-run:
publish_npm_dry_run:
name: NPM publish (dry run)
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Publish
run: pnpm publish -r --no-git-checks --dry-run
run: pnpm publish -r --no-git-checks --dry-run
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-on-crates-dry-run:
publish_crates_dry_run:
name: Crates publish (dry run)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 'stable'
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
dry-run: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
check-repo: true
publish-on-npm:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
publish_npm:
name: NPM publish
needs: [publish_npm_dry_run, publish_crates_dry_run]
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 8
run_install: false
- name: Clone root README.md on tuono package
run: cp README.md packages/tuono/README.md
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
@@ -81,17 +71,22 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-on-crates:
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
publish_crates:
name: Crates publish
needs: [publish_npm_dry_run, publish_crates_dry_run]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- uses: katyo/publish-crates@v2
with:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 'stable'
- uses: katyo/publish-crates@v2
with:
path: './crates/tuono'
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
check-repo: true
+51
View File
@@ -0,0 +1,51 @@
name: Repo root CI
# All workflows are triggered based on specific paths,
# the root repository is something that is not applicable to rust nor typescript
# so it has been moved in a standalone workflow
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
format:
name: Check format
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Test project
run: pnpm repo:root:format:check
ci_ok:
name: Repo root CI OK
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [format]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
+86
View File
@@ -0,0 +1,86 @@
name: Rust CI
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
name: Build and test crates on ${{ matrix.os }} with rust ${{ matrix.toolchain }}
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
toolchain:
- 'stable'
# Test `beta` and `nightly` toolchains only on `ubuntu`
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations
include:
- toolchain: 'beta'
os: 'ubuntu-latest'
- toolchain: 'nightly'
os: 'ubuntu-latest'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
lint_and_fmt:
name: Check format and lint rust crates
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo fmt --all -- --check
- run: cargo clippy -- -D warnings
ci_ok:
name: Rust CI OK
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [build_and_test, lint_and_fmt]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
-40
View File
@@ -1,40 +0,0 @@
name: Rust CI
on:
push:
paths:
- '.github/**'
- 'crates/**'
pull_request:
paths:
- '.github/**'
- 'crates/**'
env:
CARGO_TERM_COLOR: always
jobs:
lint_and_fmt:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo fmt --all -- --check
- run: cargo clippy -- -D warnings
build_and_test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: Build and test rust crates
runs-on: ubuntu-latest
strategy:
matrix:
toolchain:
- stable
- beta
- nightly
steps:
- uses: actions/checkout@v4
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- run: cargo build --verbose
- run: cargo test --verbose
+70
View File
@@ -0,0 +1,70 @@
name: Spell Checker
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
spell_checking:
name: LanguageTool
runs-on: ubuntu-latest
permissions:
checks: write
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Keep this value in order to show the error comments
# also on edited files.
fetch-depth: 2
# If the workflow is triggered by a push use check reporter
#
# If the workflow is triggered by a PR coming from the
# same repository, the author has write access so we can
# use pr-review reporter since the GITHUB_TOKEN will have review
# write permissions
#
# Otherwise use annotation reporter that do not require any
# particular permission
#
# Why this?
# https://github.com/Valerioageno/tuono/pull/137#pullrequestreview-2455455272
- name: Set reporter
id: reporter
env:
EVENT_NAME: ${{ github.event_name }}
FROM_MAIN_REPO: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }}
run: |
echo "EVENT_NAME: $EVENT_NAME FROM_MAIN_REPO: $FROM_MAIN_REPO"
if [ "$EVENT_NAME" == 'push' ]; then
echo "name=github-check" >> $GITHUB_OUTPUT
elif [ "$FROM_MAIN_REPO" == 'true' ]; then
echo "name=github-pr-review" >> $GITHUB_OUTPUT
else
echo "name=github-pr-annotations" >> $GITHUB_OUTPUT
fi
- name: Review dog languagetool
uses: reviewdog/action-languagetool@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review].
reporter: ${{ steps.reporter.outputs.name }}
# Change reporter level if you need.
level: 'warning'
patterns: '**/*.md **/*.txt **/*.mdx'
# enabled_categories: "STYLE,PLAIN_ENGLISH,WHITESPACE_RULE,EN_QUOTES,DASH_RULE,WORD_CONTAINS_UNDERSCORE,UPPERCASE_SENTENCE_START,ARROWS,COMMA_PARENTHESIS_WHITESPACE,UNLIKELY_OPENING_PUNCTUATION,SENTENCE_WHITESPACE,EN_UNPAIRED_BRACKETS,PHRASE_REPETITION,PUNCTUATION_PARAGRAPH_END"
disabled_categories: ''
disabled_rules: ''
+80
View File
@@ -0,0 +1,80 @@
name: Typescript CI
on:
push:
branches:
- 'main'
pull_request:
concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build_and_test:
name: Check build and test
strategy:
fail-fast: true
matrix:
os:
- 'ubuntu-latest'
- 'macos-latest'
- 'windows-latest'
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Build project
run: pnpm build
- name: Test project
run: pnpm test
lint_and_fmt:
name: Check format and lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install NodeJS Dependencies
uses: ./.github/actions/install-node-dependencies
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
ci_ok:
name: Typescript CI OK
runs-on: ubuntu-latest
timeout-minutes: 1
if: always()
needs: [build_and_test, lint_and_fmt]
env:
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
steps:
- name: Check for failure or cancelled jobs result
shell: bash
run: |
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
exit 0
else
exit 1
fi
-66
View File
@@ -1,66 +0,0 @@
name: Typescript CI
on:
push:
paths:
- '.github/**'
- 'packages/**'
pull_request:
paths:
- '.github/**'
- 'packages/**'
jobs:
build-and-test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js environment
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build project
run: pnpm build
- name: Test project
run: pnpm test
fmt-lint-and-types:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Install pnpm
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install
- name: Check formatting
run: pnpm format:check
- name: Lint
run: pnpm lint
- name: Types
run: pnpm types
+8 -2
View File
@@ -1,12 +1,14 @@
## Node related ignores
node_modules/
dist/
.turbo/
*/**/routeTree.gen.ts
pnpm-lock.yaml
examples/*/pnpm-lock.yaml
examples/playground/
vite.config.ts.timestamp*
## Rust related ignores
@@ -24,3 +26,7 @@ examples/playground/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Files, generated by the code editors
.vscode/
+1 -1
View File
@@ -1 +1 @@
v20.10.0
22.13.1
+8
View File
@@ -0,0 +1,8 @@
pnpm-lock.yaml
dist
.tuono
vite.config.ts.timestamp-*
packages/tuono-lazy-fn-vite-plugin/tests/sources/*
+1
View File
@@ -1,4 +1,5 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"trailingComma": "all"
+9
View File
@@ -0,0 +1,9 @@
* @Valerioageno @marcalexiei
# Rust
/crates/ @Valerioageno
/Cargo.toml @Valerioageno
# Misc
/.github/ @marcalexiei
+15 -15
View File
@@ -2,7 +2,7 @@
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
We, as members, contributors, and leaders, pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best, not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- The use of sexualized language or imagery, and sexual attention or
Advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct that could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -45,13 +45,13 @@ or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
not aligned with this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
An individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+16 -11
View File
@@ -1,21 +1,26 @@
![](https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/typescript.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
# How to contribute to Tuono
# How to Contribute to Tuono
## Contributions
## Contributions
Any feature contribution or suggestion is strongly appreciated.
Since the current project size there isn't yet a defined way to start a discussion. Consider to [open a new issue](https://github.com/Valerioageno/tuono/issues/new)
or to reach me using my email address [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on twitter (X) DMs @valerioageno.
We are currently managing the tasks' prioritization in the [Road to V1](https://github.com/orgs/tuono-labs/projects/2) project. You can pick any task that you see in the `ready` column that is not already assigned.
If you want to contribute on something that is in the `backlog` column, or it hasn't been scoped yet, please drop a message in the [#general](https://discord.com/invite/khQzPa654B) discord channel so we can all have visibility of it.
Consider [opening a new issue](https://github.com/tuono-labs/tuono/issues/new/choose) if you find a bug.
## Bugs
**Did you find a bug?**
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Valerioageno/tuono/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/tuono-labs/tuono/issues).
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/tuono-labs/tuono/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
**Did you write a patch that fixes a bug?**
- Open a new GitHub pull request with the patch.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
- The pull requests must pass all the CI pipelines
[Visit the documentation to learn how to open a pull request](https://tuono.dev/documentation/contributing/pull-requests)
+9 -6
View File
@@ -1,12 +1,15 @@
[workspace]
resolver = "2"
members = [
"crates/tuono",
"crates/tuono_lib",
"crates/tuono_lib_macros",
"crates/tuono",
"crates/tuono_internal",
"crates/tuono_lib",
"crates/tuono_lib_macros"
]
exclude = [
"examples/mdx",
"examples/tuono",
"examples/tutorial"
"examples/with-mdx",
"examples/tuono-app",
"examples/tuono-tutorial",
"examples/with-tailwind",
"e2e/fixtures/base",
]
+38 -74
View File
@@ -1,98 +1,62 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
</p>
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
<div align="center">
<img src="https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg" />
<img src="https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg" />
</div>
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
<br>
<br>
# Tuono
<img align="right" src="https://raw.githubusercontent.com/tuono-labs/tuono/main/assets/logo.png" width="160px">
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
![](https://github.com/tuono-labs/tuono/actions/workflows/rust.yml/badge.svg)
![](https://github.com/tuono-labs/tuono/actions/workflows/typescript.yml/badge.svg)
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
a strong focus on usability and performance.**
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
Why Tuono? Just a badass name.
> Check out the [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) to get started.
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
## Introduction
Some of its features are:
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be fullstack right? (no)**
- 🟦 Native Typescript
- 🌐 Next.js like Routing
- 🍭 CSS/SCSS modules
- 🧬 Server Side Rendering
- 🔥 Hot Module Reload
Tuono is a fullstack React framework with the server side written in Rust.
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
React is still React - it is just superpowered.
## Documentation
**Rust is an hard language then writing server side code is hard as well right? (no again)**
The [documentation](https://tuono.dev/) is available on
[tuono.dev](https://tuono.dev/).
Tuono provides a collection of utilities to handle the server side code seamlessly with the React code.
Each server side route is managed with a separate file alongside the React route. The routing is handled
by Tuono based on the files defined within the `./src/routes` directory.
## Installation
## Features
Available on MacOS, Linux and Windows.
- 🟦 Typescript
- 🌐 Routing
- 🔥 Hot Module Reload
- 🍭 CSS modules
- 🧬 Server Side Rendering
- 🧵 Multi thread backend
- ⌨️ MDX support
- ⚙️ Build optimizations
- Custom APIs*
- Image optimization*
- Server streamed content*
> *Development in progress
```sh
cargo install tuono
```
## Getting started
As already mentioned above I strongly suggest you to take a look at the
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
To download it is required [cargo](https://doc.rust-lang.org/cargo/) which is the [rust](https://www.rust-lang.org/)
package manager.
Then run `cargo install tuono`.
To list all the available commands run `tuono -h`
To create a new project run `tuono new [NAME]` (optionally you can pass the `--template` (or `-t`) flag - check the
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
Then to run the local development environment run inside the project folder `tuono dev`
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets
and to set the server project in the `production` mode.
Now to execute it just run `cargo run --release`.
## Requirements
- rust
- cargo
- node
- npm/pnpm/yarn*
> yarn [pnp](https://yarnpkg.com/features/pnp) is not supported yet
## Folder structure
Create a new project with:
```sh
tuono new [PROJECT_NAME]
```
├── package.json
├── public
├── src
│ ├── routes
│ └── styles
├── Cargo.toml
├── README.md
└── tsconfig.json
> You can optionally pass the --template (or -t) flag to directly start from a [template](https://github.com/tuono-labs/tuono/tree/main/examples)
Now just install the dependencies with you favourite JS package manager (i.e. `npm install`) and start the development environment
```sh
tuono dev
```
## Contributing
Any help or suggestion will be appreciated and encouraged.
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
## License
+18 -12
View File
@@ -1,18 +1,16 @@
[package]
name = "tuono"
version = "0.8.2"
version = "0.17.8"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = ["react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/tuono-labs/tuono"
readme = "../../README.md"
license-file = "../../LICENSE.md"
categories = ["web-programming"]
include = [
"src/*.rs",
"Cargo.toml"
]
include = ["src/*.rs", "Cargo.toml"]
[lib]
name = "tuono"
@@ -20,15 +18,23 @@ path = "src/lib.rs"
[dependencies]
clap = { version = "4.5.4", features = ["derive", "cargo"] }
watchexec = "4.0.0"
watchexec = "5.0.0"
miette = "7.2.0"
watchexec-signals = "3.0.0"
watchexec-signals = "4.0.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
watchexec-supervisor = "2.0.0"
watchexec-supervisor = "3.0.0"
glob = "0.3.1"
regex = "1.10.4"
reqwest = {version = "0.12.4", features =["blocking", "json"]}
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
serde_json = "1.0"
fs_extra = "1.3.0"
http = "1.1.0"
tuono_internal = {path = "../tuono_internal", version = "0.17.8"}
spinners = "4.1.1"
console = "0.15.10"
[dev-dependencies]
tempfile = "3.14.0"
assert_cmd = "2.0.16"
serial_test = "0.10.0"
+232 -27
View File
@@ -1,29 +1,65 @@
use crate::mode::Mode;
use glob::glob;
use glob::GlobError;
use http::Method;
use std::collections::hash_set::HashSet;
use std::collections::{hash_map::Entry, HashMap};
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::path::Path;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
use tuono_internal::config::Config;
use crate::route::Route;
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
const IGNORE_FILES: [&str; 1] = ["__root"];
const IGNORE_FILES: [&str; 1] = ["__layout"];
#[derive(Debug)]
#[cfg(target_os = "windows")]
const ROUTES_FOLDER_PATH: &str = "\\src\\routes";
#[cfg(target_os = "windows")]
const BUILD_JS_SCRIPT: &str = ".\\node_modules\\.bin\\tuono-build-prod.cmd";
#[cfg(target_os = "windows")]
const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.cmd";
#[cfg(not(target_os = "windows"))]
const ROUTES_FOLDER_PATH: &str = "/src/routes";
#[cfg(not(target_os = "windows"))]
const BUILD_JS_SCRIPT: &str = "./node_modules/.bin/tuono-build-prod";
#[cfg(not(target_os = "windows"))]
const BUILD_TUONO_CONFIG: &str = "./node_modules/.bin/tuono-build-config";
#[derive(Debug, Clone)]
pub struct App {
pub route_map: HashMap<String, Route>,
pub base_path: PathBuf,
pub has_app_state: bool,
pub config: Option<Config>,
}
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
let file = File::open(base_path.join("src/app.rs"))?;
let mut buf_reader = BufReader::new(file);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Ok(contents.contains("pub fn main"))
}
impl App {
pub fn new() -> Self {
let base_path = std::env::current_dir().unwrap();
let base_path = std::env::current_dir().expect("Failed to read current_dir");
let mut app = App {
route_map: HashMap::new(),
base_path,
base_path: base_path.clone(),
has_app_state: has_app_state(base_path).unwrap_or(false),
config: None,
};
app.collect_routes();
@@ -32,18 +68,31 @@ impl App {
}
fn collect_routes(&mut self) {
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
.expect("Failed to read glob pattern")
.for_each(|entry| {
if self.should_collect_route(&entry) {
self.collect_route(entry)
}
})
glob(
self.base_path
.join("src/routes/**/*.*")
.to_str()
.expect("Failed to glob routes folder"),
)
.expect("Failed to read glob pattern")
.for_each(|entry| {
if self.should_collect_route(&entry) {
self.collect_route(entry)
}
})
}
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
let file_extension = entry.as_ref().unwrap().extension().unwrap();
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
let file_extension = entry
.as_ref()
.unwrap()
.extension()
.expect("Failed to read file extension");
let file_name = entry
.as_ref()
.unwrap()
.file_stem()
.expect("Failed to read file name");
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
return false;
@@ -56,16 +105,23 @@ impl App {
}
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
let entry = path_buf.unwrap();
let base_path_str = self.base_path.to_str().unwrap();
let entry = path_buf.expect("Failed to read glob path");
let base_path_str = self
.base_path
.to_str()
.expect("Failed to read as str base_path");
let path = entry
.to_str()
.unwrap()
.replace(&format!("{base_path_str}/src/routes"), "")
.expect("Failed to read entry as str")
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
// Cleanup windows paths
.replace("\\", "/")
.replace(".rs", "")
.replace(".mdx", "")
.replace(".tsx", "");
if entry.extension().unwrap() == "rs" {
if entry.extension().expect("failed to read entry extension") == "rs" {
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
let mut route = Route::new(path);
route.update_axum_info();
@@ -86,10 +142,51 @@ impl App {
self.route_map.iter().any(|(_, route)| route.is_dynamic)
}
pub fn check_server_availability(&self, mode: Mode) {
// At this point the config should be available
let config = self.config.as_ref().unwrap();
let rust_listener =
std::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port));
if let Err(_e) = rust_listener {
eprintln!("Error: Failed to bind to port {}", config.server.port);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
config.server.port
);
std::process::exit(1);
}
if mode == Mode::Dev {
let vite_port = config.server.port + 1;
let vite_listener =
std::net::TcpListener::bind(format!("{}:{}", config.server.host, vite_port));
if let Err(_e) = vite_listener {
eprintln!("Error: Failed to bind to port {}", vite_port);
eprintln!(
"Please ensure that port {} is not already in use by another process or application.",
vite_port
);
std::process::exit(1);
}
}
}
pub fn build_react_prod(&self) {
Command::new("./node_modules/.bin/tuono-build-prod")
if !Path::new(BUILD_JS_SCRIPT).exists() {
eprintln!("Failed to find the build script. Please run `npm install`");
std::process::exit(1);
}
let output = Command::new(BUILD_JS_SCRIPT)
.output()
.expect("Failed to build the react source");
if !output.status.success() {
eprintln!("Failed to build the react source");
eprintln!("Error: {}", String::from_utf8_lossy(&output.stderr));
std::process::exit(1);
}
}
pub fn run_rust_server(&self) -> Child {
@@ -101,6 +198,44 @@ impl App {
.spawn()
.expect("Failed to run the rust server")
}
pub fn build_tuono_config(&mut self) -> Result<std::process::Output, std::io::Error> {
if !Path::new(BUILD_TUONO_CONFIG).exists() {
eprintln!("Failed to find the build script. Please run `npm install`");
std::process::exit(1);
}
let config_build_command = Command::new(BUILD_TUONO_CONFIG)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
if let Ok(config) = Config::get() {
self.config = Some(config);
} else {
eprintln!("[CLI] Failed to read tuono.config.ts");
std::process::exit(1);
};
config_build_command
}
pub fn get_used_http_methods(&self) -> HashSet<Method> {
let mut acc = HashSet::new();
for (_, route) in self.route_map.clone().into_iter() {
if route.axum_info.is_some() {
acc.insert(Method::GET);
}
if !route.is_api() {
continue;
}
for method in route.api_data.unwrap().methods.into_iter() {
acc.insert(method);
}
}
acc
}
}
#[cfg(test)]
@@ -111,14 +246,36 @@ mod tests {
#[test]
fn should_collect_routes() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this\\[post].rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\UPPERCASE.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\sitemap.xml.rs",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
"/home/user/Documents/tuono/src/routes/posts/index.rs",
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
"/home/user/Documents/tuono/src/routes/posts/handle-this.rs",
"/home/user/Documents/tuono/src/routes/posts/handle-this/[post].rs",
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
"/home/user/Documents/tuono/src/routes/sitemap.xml.rs",
];
routes
@@ -130,7 +287,13 @@ mod tests {
("/about", "about"),
("/posts/index", "posts_index"),
("/posts/[post]", "posts_dyn_post"),
("/posts/handle-this", "posts_handle_hyphen_this"),
(
"/posts/handle-this/[post]",
"posts_handle_hyphen_this_dyn_post",
),
("/posts/UPPERCASE", "posts_uppercase"),
("/sitemap.xml", "sitemap_dot_xml"),
];
results.into_iter().for_each(|(path, module_import)| {
@@ -150,8 +313,25 @@ mod tests {
#[test]
fn should_create_multi_level_axum_paths() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\any-post.rs",
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/index.rs",
@@ -176,7 +356,7 @@ mod tests {
assert_eq!(
app.route_map
.get(path)
.unwrap()
.expect("Failed to get route path")
.axum_info
.as_ref()
.unwrap()
@@ -212,8 +392,8 @@ mod tests {
app.base_path = "/home/user/Documents/tuono".into();
let routes = [
"/home/user/Documents/tuono/src/routes/__root.tsx",
"/home/user/Documents/tuono/src/routes/posts/__root.tsx",
"/home/user/Documents/tuono/src/routes/__layout.tsx",
"/home/user/Documents/tuono/src/routes/posts/__layout.tsx",
];
routes.into_iter().for_each(|route| {
@@ -228,8 +408,23 @@ mod tests {
#[test]
fn should_correctly_parse_routes_with_server_handler() {
let mut app = App::new();
app.base_path = "/home/user/Documents/tuono".into();
#[cfg(target_os = "windows")]
let base_path = "\\home\\user\\Documents\\tuono";
#[cfg(not(target_os = "windows"))]
let base_path = "/home/user/Documents/tuono";
app.base_path = base_path.into();
#[cfg(target_os = "windows")]
let routes = [
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
"\\home\\user\\Documents/tuono\\src\\routes\\about.tsx",
"\\home\\user\\Documents\\tuono\\src\\routes\\index.tsx",
];
#[cfg(not(target_os = "windows"))]
let routes = [
"/home/user/Documents/tuono/src/routes/about.rs",
"/home/user/Documents/tuono/src/routes/about.tsx",
@@ -246,9 +441,19 @@ mod tests {
.into_iter()
.for_each(|(path, expected_has_server_handler)| {
if expected_has_server_handler {
assert!(app.route_map.get(path).unwrap().axum_info.is_some())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_some())
} else {
assert!(app.route_map.get(path).unwrap().axum_info.is_none())
assert!(app
.route_map
.get(path)
.expect("Failed to get route path")
.axum_info
.is_none())
}
})
}
+50 -13
View File
@@ -20,6 +20,10 @@ enum Actions {
#[arg(short, long = "static")]
/// Statically generate the website HTML
ssg: bool,
#[arg(short, long)]
/// Prevent to export the js assets
no_js_emit: bool,
},
/// Scaffold a new project
New {
@@ -29,22 +33,33 @@ enum Actions {
/// examples
#[arg(short, long)]
template: Option<String>,
/// Load the latest commit available on the main branch
#[arg(long)]
head: Option<bool>,
},
}
#[derive(Parser, Debug)]
#[command(version, about = "The react/rust fullstack framework")]
#[command(version, about = "The React/Rust full-stack framework")]
struct Args {
#[command(subcommand)]
action: Actions,
}
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
fn check_for_tuono_config() {
if !PathBuf::from("tuono.config.ts").exists() {
eprintln!("Cannot find tuono.config.ts - is this a tuono project?");
std::process::exit(1);
}
}
fn init_tuono_folder(mode: Mode) -> std::io::Result<App> {
check_for_tuono_config();
check_tuono_folder()?;
bundle_axum_source(mode)?;
let app = bundle_axum_source(mode)?;
create_client_entry_files()?;
Ok(())
Ok(app)
}
pub fn app() -> std::io::Result<()> {
@@ -52,12 +67,22 @@ pub fn app() -> std::io::Result<()> {
match args.action {
Actions::Dev => {
init_tuono_folder(Mode::Dev)?;
let mut app = init_tuono_folder(Mode::Dev)?;
app.build_tuono_config()
.expect("Failed to build tuono.config.ts");
app.check_server_availability(Mode::Dev);
watch::watch().unwrap();
}
Actions::Build { ssg } => {
init_tuono_folder(Mode::Prod)?;
let app = App::new();
Actions::Build { ssg, no_js_emit } => {
let mut app = init_tuono_folder(Mode::Prod)?;
if no_js_emit {
println!("Rust build successfully finished");
return Ok(());
}
if ssg && app.has_dynamic_routes() {
// TODO: allow dynamic routes static generation
@@ -65,6 +90,11 @@ pub fn app() -> std::io::Result<()> {
return Ok(());
}
app.build_tuono_config()
.expect("Failed to build tuono.config.ts");
app.check_server_availability(Mode::Prod);
app.build_react_prod();
if ssg {
@@ -86,6 +116,8 @@ pub fn app() -> std::io::Result<()> {
)
.expect("Failed to clone assets into static output folder");
// the process is killed below so we don't really need to use wait() function
#[allow(clippy::zombie_processes)]
let mut rust_server = app.run_rust_server();
let reqwest = reqwest::blocking::Client::builder()
@@ -96,17 +128,21 @@ pub fn app() -> std::io::Result<()> {
// Wait for server
let mut is_server_ready = false;
let config = app.config.as_ref().unwrap();
while !is_server_ready {
if reqwest.get("http://localhost:3000").send().is_ok() {
let server_url =
format!("http://{}:{}", config.server.host, config.server.port);
if reqwest.get(server_url).send().is_ok() {
is_server_ready = true
}
// TODO: add maximum tries
sleep(Duration::from_secs(1))
}
app.route_map
.iter()
.for_each(|(_, route)| route.save_ssg_html(&reqwest));
for (_, route) in app.route_map {
route.save_ssg_file(&reqwest)
}
// Close server
let _ = rust_server.kill();
@@ -117,8 +153,9 @@ pub fn app() -> std::io::Result<()> {
Actions::New {
folder_name,
template,
head,
} => {
scaffold_project::create_new_project(folder_name, template);
scaffold_project::create_new_project(folder_name, template, head);
}
}
+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
@@ -1,4 +1,4 @@
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum Mode {
Prod,
Dev,
+118 -19
View File
@@ -1,17 +1,19 @@
use fs_extra::dir::create_all;
use http::Method;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::Url;
use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::str::FromStr;
fn has_dynamic_path(route: &str) -> bool {
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
regex.is_match(route)
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AxumInfo {
// Path for importing the module
pub module_import: String,
@@ -20,44 +22,105 @@ pub struct AxumInfo {
}
impl AxumInfo {
pub fn new(path: String) -> Self {
pub fn new(route: &Route) -> Self {
// Remove first slash
let mut module = path.chars();
let mut module = route.path.chars();
module.next();
let axum_route = path.replace("/index", "");
let axum_route = route.path.replace("/index", "");
let module_import = module
.as_str()
.to_string()
.replace('/', "_")
.replace('.', "_dot_")
.replace('-', "_hyphen_")
.to_lowercase();
if axum_route.is_empty() {
return AxumInfo {
module_import: module.as_str().to_string().replace('/', "_"),
module_import,
axum_route: "/".to_string(),
};
}
if has_dynamic_path(&path) {
if route.is_dynamic {
return AxumInfo {
module_import: module
.as_str()
.to_string()
.replace('/', "_")
.replace('-', "_hyphen_")
.replace('[', "dyn_")
.replace("...", "catch_all_")
.replace(']', ""),
axum_route: axum_route
.replace("[...", "*")
.replace('[', ":")
.replace(']', ""),
axum_route: axum_route.replace('[', ":").replace(']', ""),
};
}
AxumInfo {
module_import: module.as_str().to_string().replace('/', "_").to_lowercase(),
module_import,
axum_route,
}
}
}
#[derive(Debug, PartialEq, Eq)]
// TODO: to be extended with common scenarios
const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"];
// TODO: Refine this function to catch
// if the methods are commented.
fn read_http_methods_from_file(path: &String) -> Vec<Method> {
let regex = Regex::new(r"tuono_lib::api\((.*?)\)]").expect("Failed to create API regex");
let file = fs_extra::file::read_to_string(path).expect("Failed to read API file");
regex
.find_iter(&file)
.map(|proc_macro| {
let http_method = proc_macro
.as_str()
// Extract just the element surrounded by the phrantesist.
.replace("tuono_lib::api(", "")
.replace(")]", "");
Method::from_str(http_method.as_str()).unwrap_or(Method::GET)
})
.collect::<Vec<Method>>()
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ApiData {
pub methods: Vec<Method>,
}
impl ApiData {
pub fn new(path: &String) -> Option<Self> {
if !path.starts_with("/api/") {
return None;
}
let base_path = std::env::current_dir().expect("Failed to get the base_path");
let file_path = base_path
.join(format!("src/routes{path}.rs"))
.to_str()
.unwrap()
.to_string();
let methods = read_http_methods_from_file(&file_path);
Some(ApiData { methods })
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Route {
path: String,
pub is_dynamic: bool,
pub axum_info: Option<AxumInfo>,
pub api_data: Option<ApiData>,
}
impl Route {
@@ -66,14 +129,19 @@ impl Route {
path: cleaned_path.clone(),
axum_info: None,
is_dynamic: has_dynamic_path(&cleaned_path),
api_data: ApiData::new(&cleaned_path),
}
}
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self.path.clone()))
pub fn is_api(&self) -> bool {
self.api_data.is_some()
}
pub fn save_ssg_html(&self, reqwest: &Client) {
pub fn update_axum_info(&mut self) {
self.axum_info = Some(AxumInfo::new(self))
}
pub fn save_ssg_file(&self, reqwest: &Client) {
let path = &self.path.replace("index", "");
let mut response = reqwest
@@ -81,7 +149,7 @@ impl Route {
.send()
.unwrap();
let file_path = PathBuf::from(format!("out/static{}.html", &self.path));
let file_path = self.output_file_path();
let parent_dir = file_path.parent().unwrap();
@@ -95,8 +163,7 @@ impl Route {
// Saving also the server response
if self.axum_info.is_some() {
let data_file_path =
PathBuf::from(&format!("out/static/__tuono/data{}/data.json", path));
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}"));
let data_parent_dir = data_file_path.parent().unwrap();
@@ -105,11 +172,11 @@ impl Route {
.expect("Failed to create data parent directories");
}
let base = Url::parse("http://localhost:3000/__tuono/data/data.json").unwrap();
let base = Url::parse("http://localhost:3000/__tuono/data").unwrap();
let path = if path == "/" { "" } else { path };
let pathname = &format!("/__tuono/data{path}/data.json");
let pathname = &format!("/__tuono/data{path}");
let url = base
.join(pathname)
@@ -123,6 +190,19 @@ impl Route {
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
}
}
fn output_file_path(&self) -> PathBuf {
let cleaned_path = self.path.replace("index", "");
if NO_HTML_EXTENSIONS
.iter()
.any(|extension| self.path.ends_with(extension))
{
return PathBuf::from(format!("out/static{}", cleaned_path));
}
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
}
}
#[cfg(test)]
@@ -152,14 +232,33 @@ mod tests {
#[test]
fn should_correctly_create_the_axum_infos() {
let info = AxumInfo::new("/index".to_string());
let info = AxumInfo::new(&Route::new("/index".to_string()));
assert_eq!(info.axum_route, "/");
assert_eq!(info.module_import, "index");
let dyn_info = AxumInfo::new("/[posts]".to_string());
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string()));
assert_eq!(dyn_info.axum_route, "/:posts");
assert_eq!(dyn_info.module_import, "dyn_posts");
}
#[test]
fn should_define_the_correct_html_build_path() {
let routes = [
("/index", "out/static/index.html"),
("/documentation", "out/static/documentation/index.html"),
("/sitemap.xml", "out/static/sitemap.xml"),
(
"/documentation/routing",
"out/static/documentation/routing/index.html",
),
];
for (path, html) in routes {
let route = Route::new(path.to_string());
assert_eq!(route.output_file_path(), PathBuf::from(html))
}
}
}
+155 -32
View File
@@ -1,15 +1,18 @@
use clap::crate_version;
use reqwest::blocking;
use reqwest::blocking::Client;
use serde::Deserialize;
use std::env;
use std::fs::{self, create_dir, File, OpenOptions};
use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
const GITHUB_TUONO_REPO_URL: &str =
"https://api.github.com/repos/Valerioageno/tuono/git/trees/main?recursive=1";
const GITHUB_TUONO_TAGS_URL: &str = "https://api.github.com/repos/tuono-labs/tuono/git/ref/tags/";
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/Valerioageno/tuono/main/";
const GITHUB_TUONO_TAG_COMMIT_TREES_URL: &str =
"https://api.github.com/repos/tuono-labs/tuono/git/trees/";
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/tuono-labs/tuono";
#[derive(Deserialize, Debug)]
enum GithubFileType {
@@ -20,10 +23,25 @@ enum GithubFileType {
}
#[derive(Deserialize, Debug)]
struct GithubResponse<T> {
struct GithubTagObject {
sha: String,
}
#[derive(Deserialize, Debug)]
struct GithubTagResponse {
object: GithubTagObject,
}
#[derive(Deserialize, Debug)]
struct GithubTreeResponse<T> {
tree: Vec<T>,
}
fn exit_with_error(message: &str) -> ! {
eprintln!("{}", message);
std::process::exit(1);
}
#[derive(Deserialize, Debug)]
struct GithubFile {
path: String,
@@ -32,42 +50,66 @@ struct GithubFile {
}
fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
let mut file = File::create(path)?;
let mut file = File::create(&path).unwrap_or_else(|err| {
exit_with_error(&format!(
"Failed to create file {}: {}",
path.display(),
err
));
});
let _ = file.write_all(content.as_bytes());
Ok(())
}
pub fn create_new_project(folder_name: Option<String>, template: Option<String>) {
pub fn create_new_project(
folder_name: Option<String>,
template: Option<String>,
select_head: Option<bool>,
) {
let folder = folder_name.unwrap_or(".".to_string());
// In case of missing select the tuono example
let template = template.unwrap_or("tuono".to_string());
let template = template.unwrap_or("tuono-app".to_string());
let client = blocking::Client::builder()
.user_agent("")
.build()
.expect("Failed to build reqwest client");
.unwrap_or_else(|_| exit_with_error("Error: Failed to build request client"));
let res = client
.get(GITHUB_TUONO_REPO_URL)
// This string does not include the "v" version prefix
let cli_version: &str = crate_version!();
let tree_url: String = generate_tree_url(select_head, &client, cli_version);
let res_tree = client
.get(tree_url)
.send()
.expect("Failed to call the folder github API")
.json::<GithubResponse<GithubFile>>()
.expect("Failed to parse the repo structure");
.unwrap_or_else(|_| {
exit_with_error(&format!(
"Failed to call the tagged commit tree github API for v{cli_version}"
))
})
.json::<GithubTreeResponse<GithubFile>>()
.expect("Failed to parse the tree structure");
let new_project_files = res
let new_project_files = res_tree
.tree
.iter()
.filter(|GithubFile { path, .. }| path.starts_with(&format!("examples/{template}/")))
.collect::<Vec<&GithubFile>>();
if new_project_files.is_empty() {
println!("Template not found: {template}");
return;
eprintln!("Error: Template '{template}' not found");
println!("Hint: you can view the available templates at https://github.com/tuono-labs/tuono/tree/main/examples");
std::process::exit(1);
}
if folder != "." {
if Path::new(&folder).exists() {
eprintln!("Error: Directory '{folder}' already exists");
println!("Hint: you can scaffold a tuono project within an existing folder with 'cd {folder} && tuono new .'");
std::process::exit(1);
}
create_dir(&folder).unwrap();
}
@@ -77,25 +119,33 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
let folder_path = current_dir.join(folder_name);
create_directories(&new_project_files, &folder_path, &template)
.expect("Failed to create directories");
.unwrap_or_else(|err| exit_with_error(&format!("Failed to create directories: {}", err)));
for GithubFile {
element_type, path, ..
} in new_project_files.iter()
{
if let GithubFileType::Blob = element_type {
let url = generate_raw_content_url(select_head, cli_version, path);
let file_content = client
.get(format!("{GITHUB_RAW_CONTENT_URL}{path}"))
.get(url)
.send()
.expect("Failed to call the folder github API")
.text()
.expect("Failed to parse the repo structure");
.map_err(|_| exit_with_error("Failed to call the folder github API"))
.and_then(|response| {
response
.text()
.map_err(|_| exit_with_error("Failed to parse the repo structure"))
})
.unwrap();
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
let file_path = folder_path.join(&path);
create_file(file_path, file_content).expect("failed to create file");
if let Err(err) = create_file(file_path, file_content) {
exit_with_error(&format!("Failed to create file: {}", err));
}
}
}
@@ -104,6 +154,36 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
outro(folder);
}
fn generate_raw_content_url(select_head: Option<bool>, cli_version: &str, path: &String) -> String {
let tag = if select_head.unwrap_or(false) {
"main"
} else {
&format!("v{cli_version}")
};
format!("{}/{}/{}", GITHUB_RAW_CONTENT_URL, tag, path)
}
fn generate_tree_url(select_head: Option<bool>, client: &Client, cli_version: &str) -> String {
if select_head.unwrap_or(false) {
format!("{}main?recursive=1", GITHUB_TUONO_TAG_COMMIT_TREES_URL)
} else {
// This string does not include the "v" version prefix
let res_tag = client
.get(format!("{}v{}", GITHUB_TUONO_TAGS_URL, cli_version))
.send()
.unwrap_or_else(|_| {
exit_with_error("Failed to call the tag github API for v{cli_version}")
})
.json::<GithubTagResponse>()
.unwrap_or_else(|_| exit_with_error("Failed to parse the tag response"));
format!(
"{}{}?recursive=1",
GITHUB_TUONO_TAG_COMMIT_TREES_URL, res_tag.object.sha
)
}
}
fn create_directories(
new_project_files: &[&GithubFile],
folder_path: &Path,
@@ -122,19 +202,23 @@ fn create_directories(
}
Ok(())
}
fn update_package_json_version(folder_path: &Path) -> io::Result<()> {
let v = crate_version!();
let package_json_path = folder_path.join(PathBuf::from("package.json"));
let package_json = fs::read_to_string(&package_json_path)?;
let package_json = fs::read_to_string(&package_json_path)
.unwrap_or_else(|err| exit_with_error(&format!("Failed to read package.json: {}", err)));
let package_json = package_json.replace("link:../../packages/tuono", v);
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(package_json_path)?;
.open(package_json_path)
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open package.json: {}", err)));
file.write_all(package_json.as_bytes())?;
file.write_all(package_json.as_bytes())
.unwrap_or_else(|err| {
exit_with_error(&format!("Failed to write to package.json: {}", err))
});
Ok(())
}
@@ -142,16 +226,21 @@ fn update_package_json_version(folder_path: &Path) -> io::Result<()> {
fn update_cargo_toml_version(folder_path: &Path) -> io::Result<()> {
let v = crate_version!();
let cargo_toml_path = folder_path.join(PathBuf::from("Cargo.toml"));
let cargo_toml = fs::read_to_string(&cargo_toml_path)?;
let cargo_toml =
cargo_toml.replace("{ path = \"../../crates/tuono_lib/\"}", &format!("\"{v}\""));
let cargo_toml = fs::read_to_string(&cargo_toml_path)
.unwrap_or_else(|err| exit_with_error(&format!("Failed to read Cargo.toml: {}", err)));
let cargo_toml = cargo_toml.replace(
"{ path = \"../../crates/tuono_lib/\" }",
&format!("\"{v}\""),
);
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(cargo_toml_path)?;
.open(cargo_toml_path)
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open Cargo.toml: {}", err)));
file.write_all(cargo_toml.as_bytes())?;
file.write_all(cargo_toml.as_bytes())
.unwrap_or_else(|err| exit_with_error(&format!("Failed to write to Cargo.toml: {}", err)));
Ok(())
}
@@ -170,3 +259,37 @@ fn outro(folder_name: String) {
println!("\nRun the local environment:");
println!("tuono dev");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_valid_content_url_from_head() {
let expected = format!(
"{}/{}/{}",
GITHUB_RAW_CONTENT_URL, "main", "examples/tuono-app"
);
let generated = generate_raw_content_url(
Some(true),
crate_version!(),
&String::from("examples/tuono-app"),
);
assert_eq!(expected, generated)
}
#[test]
fn generate_valid_content_url_from_cli_version() {
let expected = format!(
"{}/{}/{}",
GITHUB_RAW_CONTENT_URL,
&format!("v{}", crate_version!()),
"examples/tuono-app"
);
let generated = generate_raw_content_url(
Some(false),
crate_version!(),
&String::from("examples/tuono-app"),
);
assert_eq!(expected, generated)
}
}
+67 -26
View File
@@ -4,6 +4,8 @@ use std::io;
use std::io::prelude::*;
use std::path::Path;
use clap::crate_version;
use crate::app::App;
use crate::mode::Mode;
use crate::route::AxumInfo;
@@ -33,20 +35,27 @@ pub const AXUM_ENTRY_POINT: &str = r##"
// File automatically generated
// Do not manually change it
use tuono_lib::{tokio, Mode, Server, axum::Router};
use tuono_lib::{tokio, Mode, Server, axum::Router, tuono_internal_init_v8_platform};
// AXUM_GET_ROUTE_HANDLER
const MODE: Mode = /*MODE*/;
// MODULE_IMPORTS
//MAIN_FILE_IMPORT//
#[tokio::main]
async fn main() {
tuono_internal_init_v8_platform();
println!("\n ⚡ Tuono v/*VERSION*/");
//MAIN_FILE_DEFINITION//
let router = Router::new()
// ROUTE_BUILDER
;
//MAIN_FILE_USAGE//;
Server::init(router, MODE).start().await
Server::init(router, MODE).await.start().await
}
"##;
@@ -74,13 +83,22 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
module_import,
} = axum_info.as_ref().unwrap();
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::route))"#
));
let slash = if axum_route.ends_with('/') { "" } else { "/" };
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
));
if !route.is_api() {
route_declarations.push_str(&format!(
r#".route("{axum_route}", get({module_import}::tuono__internal__route))"#
));
route_declarations.push_str(&format!(
r#".route("/__tuono/data{axum_route}", get({module_import}::tuono__internal__api))"#
));
} else {
for method in route.api_data.as_ref().unwrap().methods.clone() {
let method = method.to_string().to_lowercase();
route_declarations.push_str(&format!(
r#".route("{axum_route}", {method}({module_import}::{method}__tuono_internal_api))"#
));
}
}
}
}
@@ -105,7 +123,7 @@ fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
route_declarations
}
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
pub fn bundle_axum_source(mode: Mode) -> io::Result<App> {
let base_path = std::env::current_dir().unwrap();
let app = App::new();
@@ -114,7 +132,7 @@ pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
create_main_file(&base_path, &bundled_file);
Ok(())
Ok(app)
}
fn generate_axum_source(app: &App, mode: Mode) -> String {
@@ -127,22 +145,45 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
"// MODULE_IMPORTS\n",
&create_modules_declaration(&app.route_map),
)
.replace("/*MODE*/", mode.as_str());
let has_server_handlers = app
.route_map
.iter()
.filter(|(_, route)| route.axum_info.is_some())
.collect::<Vec<(&String, &Route)>>()
.is_empty();
if !has_server_handlers {
return src.replace(
"// AXUM_GET_ROUTE_HANDLER",
"use tuono_lib::axum::routing::get;",
.replace("/*VERSION*/", crate_version!())
.replace("/*MODE*/", mode.as_str())
.replace(
"//MAIN_FILE_IMPORT//",
if app.has_app_state {
r#"#[path="../src/app.rs"]
mod tuono_main_state;
"#
} else {
""
},
)
.replace(
"//MAIN_FILE_DEFINITION//",
if app.has_app_state {
"let user_custom_state = tuono_main_state::main();"
} else {
""
},
)
.replace(
"//MAIN_FILE_USAGE//",
if app.has_app_state {
".with_state(user_custom_state)"
} else {
""
},
);
let mut import_http_handler = String::new();
let used_http_methods = app.get_used_http_methods();
for method in used_http_methods.into_iter() {
let method = method.to_string().to_lowercase();
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
}
src
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
}
pub fn check_tuono_folder() -> io::Result<()> {
+54 -12
View File
@@ -1,3 +1,4 @@
use std::path::Path;
use std::sync::Arc;
use watchexec_supervisor::command::{Command, Program};
@@ -8,11 +9,27 @@ use watchexec_supervisor::job::{start_job, Job};
use crate::mode::Mode;
use crate::source_builder::bundle_axum_source;
use console::Term;
use spinners::{Spinner, Spinners};
#[cfg(target_os = "windows")]
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
#[cfg(target_os = "windows")]
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
#[cfg(not(target_os = "windows"))]
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
#[cfg(not(target_os = "windows"))]
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
fn watch_react_src() -> Job {
if !Path::new(DEV_SSR_BIN_SRC).exists() {
eprintln!("Failed to find script to run dev watch. Please run `npm install`");
std::process::exit(1);
}
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-watch".into(),
prog: DEV_WATCH_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -20,7 +37,7 @@ fn watch_react_src() -> Job {
.0
}
fn build_rust_src() -> Job {
fn run_rust_dev_server() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "cargo".into(),
@@ -31,10 +48,25 @@ fn build_rust_src() -> Job {
.0
}
fn build_react_ssr_src() -> Job {
fn build_rust_src() -> Job {
start_job(Arc::new(Command {
program: Program::Exec {
prog: "node_modules/.bin/tuono-dev-ssr".into(),
prog: "cargo".into(),
args: vec!["build".to_string(), "-q".to_string()],
},
options: Default::default(),
}))
.0
}
fn build_react_ssr_src() -> Job {
if !Path::new(DEV_SSR_BIN_SRC).exists() {
eprintln!("Failed to find script to run dev ssr. Please run `npm install`");
std::process::exit(1);
}
start_job(Arc::new(Command {
program: Program::Exec {
prog: DEV_SSR_BIN_SRC.into(),
args: vec![],
},
options: Default::default(),
@@ -44,18 +76,28 @@ fn build_react_ssr_src() -> Job {
#[tokio::main]
pub async fn watch() -> Result<()> {
println!("Starting development environment...");
let term = Term::stdout();
let mut sp = Spinner::new(Spinners::Dots, "Starting dev server...".into());
watch_react_src().start().await;
let run_server = build_rust_src();
let rust_server = run_rust_dev_server();
let build_rust_src = build_rust_src();
let build_ssr_bundle = build_react_ssr_src();
build_ssr_bundle.start().await;
build_rust_src.start().await;
run_server.start().await;
// Wait vite and rust builds
build_ssr_bundle.to_wait().await;
build_rust_src.to_wait().await;
rust_server.start().await;
// Remove the spinner
sp.stop();
term.clear_line().unwrap();
let wx = Watchexec::new(move |mut action| {
let mut should_reload_ssr_bundle = false;
@@ -76,10 +118,10 @@ pub async fn watch() -> Result<()> {
}
if should_reload_rust_server {
println!("Reloading...");
run_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
run_server.start();
println!(" Reloading...");
rust_server.stop();
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
rust_server.start();
}
if should_reload_ssr_bundle {
+202
View File
@@ -0,0 +1,202 @@
mod utils;
use assert_cmd::Command;
use serial_test::serial;
use std::fs;
use utils::TempTuonoProject;
const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]";
const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]";
#[cfg(target_os = "windows")]
const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.cmd";
#[cfg(not(target_os = "windows"))]
const BUILD_TUONO_CONFIG: &str = "./node_modules/.bin/tuono-build-config";
#[test]
#[serial]
fn it_successfully_create_the_index_route() {
let temp_tuono_project = TempTuonoProject::new();
temp_tuono_project.add_file("./src/routes/index.rs");
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.arg("--no-js-emit")
.assert()
.success();
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
let temp_main_rs_content =
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/index.rs"]"#));
assert!(temp_main_rs_content.contains("mod index;"));
assert!(temp_main_rs_content
.contains(r#".route("/", get(index::tuono__internal__route)).route("/__tuono/data/", get(index::tuono__internal__api))"#));
}
#[test]
#[serial]
fn it_successfully_create_an_api_route() {
let temp_tuono_project = TempTuonoProject::new();
temp_tuono_project.add_file_with_content("./src/routes/api/health_check.rs", POST_API_FILE);
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.arg("--no-js-emit")
.assert()
.success();
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
let temp_main_rs_content =
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
dbg!(&temp_main_rs_content);
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
assert!(temp_main_rs_content.contains("mod api_health_check;"));
assert!(temp_main_rs_content.contains(
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
));
}
#[test]
#[serial]
fn it_successfully_create_multiple_api_for_the_same_file() {
let temp_tuono_project = TempTuonoProject::new();
temp_tuono_project.add_file_with_content(
"./src/routes/api/health_check.rs",
&format!("{POST_API_FILE}{GET_API_FILE}"),
);
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.arg("--no-js-emit")
.assert()
.success();
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
let temp_main_rs_content =
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/health_check.rs"]"#));
assert!(temp_main_rs_content.contains("mod api_health_check;"));
assert!(temp_main_rs_content.contains(
r#".route("/api/health_check", post(api_health_check::post__tuono_internal_api))"#
));
assert!(temp_main_rs_content.contains(
r#".route("/api/health_check", get(api_health_check::get__tuono_internal_api))"#
));
}
#[test]
#[serial]
fn it_successfully_create_catch_all_routes() {
let temp_tuono_project = TempTuonoProject::new();
temp_tuono_project.add_file("./src/routes/[...all_routes].rs");
temp_tuono_project.add_file_with_content("./src/routes/api/[...all_apis].rs", POST_API_FILE);
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.arg("--no-js-emit")
.assert()
.success();
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
let temp_main_rs_content =
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(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(
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("/__tuono/data/*all_routes", get(dyn_catch_all_all_routes::tuono__internal__api))"#));
}
#[test]
#[serial]
fn it_fails_without_installed_build_config_script() {
let _guard = TempTuonoProject::new();
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.assert()
.failure()
.stderr("Failed to find the build script. Please run `npm install`\n");
}
#[test]
#[serial]
fn it_fails_without_installed_build_script() {
let temp_tuono_project = TempTuonoProject::new();
temp_tuono_project.add_file_with_content(BUILD_TUONO_CONFIG, "#!/bin/bash");
Command::new("chmod")
.arg("+x")
.arg(BUILD_TUONO_CONFIG)
.assert()
.success();
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("build")
.assert()
.failure()
.stderr("[CLI] Failed to read tuono.config.ts\n");
}
#[test]
#[serial]
fn dev_fails_with_no_config() {
let _temp_tuono_project = TempTuonoProject::new_with_no_config();
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("dev")
.assert()
.failure()
.stderr("Cannot find tuono.config.ts - is this a tuono project?\n");
}
#[test]
#[serial]
fn build_fails_with_no_config() {
let _temp_tuono_project = TempTuonoProject::new_with_no_config();
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
test_tuono_build
.arg("dev")
.assert()
.failure()
.stderr("Cannot find tuono.config.ts - is this a tuono project?\n");
}
+69
View File
@@ -0,0 +1,69 @@
use fs_extra::dir::create_all;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use tempfile::{tempdir, TempDir};
#[derive(Debug)]
pub struct TempTuonoProject {
original_dir: PathBuf,
temp_dir: TempDir,
}
impl TempTuonoProject {
pub fn new() -> Self {
let project = TempTuonoProject::new_with_no_config();
project.add_file("./tuono.config.ts");
project
}
pub fn new_with_no_config() -> Self {
let original_dir = env::current_dir().expect("Failed to read current_dir");
let temp_dir = tempdir().expect("Failed to create temp_dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
TempTuonoProject {
original_dir,
temp_dir,
}
}
pub fn path(&self) -> &Path {
self.temp_dir.path()
}
pub fn add_file(&self, path: &str) -> File {
let path = PathBuf::from(path);
create_all(
path.parent().expect("Route path does not have any parent"),
false,
)
.expect("Failed to create parent route directory");
File::create(path).expect("Failed to create the route file")
}
pub fn add_file_with_content<'a>(&self, path: &'a str, content: &'a str) {
let path = PathBuf::from(path);
create_all(
path.parent().expect("Route path does not have any parent"),
false,
)
.expect("Failed to create parent route directory");
let mut file = File::create(path).expect("Failed to create the route file");
file.write_all(content.as_bytes())
.expect("Failed to write into API file");
}
}
impl Drop for TempTuonoProject {
fn drop(&mut self) {
// Set back the current dir in the previous state
env::set_current_dir(&self.original_dir)
.expect("Failed to restore the original directory.");
}
}
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "tuono_internal"
version = "0.17.8"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/tuono-labs/tuono"
readme = "../../README.md"
license-file = "../../LICENSE.md"
categories = ["web-programming"]
include = [
"src/*.rs",
"Cargo.toml"
]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.137"
[dev-dependencies]
fs_extra = "1.3.0"
tempfile = "3.14.0"
assert_cmd = "2.0.16"
serial_test = "0.10.0"
+4
View File
@@ -0,0 +1,4 @@
# tuono_internal
This crate is a collection of internal utilities for the Tuono project. It is
not intended for use outside of the project.
+24
View File
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io;
use std::path::PathBuf;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Config {
pub server: ServerConfig,
}
impl Config {
pub fn get() -> io::Result<Config> {
let config_file = read_to_string(PathBuf::from_iter([".tuono", "config", "config.json"]))?;
serde_json::from_str(&config_file)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
+1
View File
@@ -0,0 +1 @@
pub mod config;
+50
View File
@@ -0,0 +1,50 @@
mod utils;
use serial_test::serial;
use std::io::ErrorKind;
use tuono_internal::config::Config;
use utils::TempTuonoProject;
#[test]
#[serial]
fn should_correctly_read_the_config_file() {
let folder = TempTuonoProject::new();
folder.add_file_with_content(
"./.tuono/config/config.json",
r#"{ "server": {"host": "localhost", "port": 3000}}"#,
);
let config = Config::get();
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.server.host, "localhost");
assert_eq!(config.server.port, 3000);
}
#[test]
#[serial]
fn should_fail_if_the_file_does_not_exist() {
TempTuonoProject::new();
let config = Config::get();
assert!(config.is_err());
assert_eq!(config.err().unwrap().kind(), ErrorKind::NotFound);
}
#[test]
#[serial]
fn should_fail_if_the_file_is_not_json() {
let folder = TempTuonoProject::new();
folder.add_file_with_content("./.tuono/config/config.json", "INVALID JSON");
let config = Config::get();
assert!(config.is_err());
assert_eq!(config.err().unwrap().kind(), ErrorKind::InvalidData);
}
+49
View File
@@ -0,0 +1,49 @@
use fs_extra::dir::create_all;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::{tempdir, TempDir};
#[derive(Debug)]
pub struct TempTuonoProject {
original_dir: PathBuf,
#[allow(dead_code)]
// Required for dropping the temp_dir when this struct drops
temp_dir: TempDir,
}
impl TempTuonoProject {
pub fn new() -> Self {
let original_dir = env::current_dir().expect("Failed to read current_dir");
let temp_dir = tempdir().expect("Failed to create temp_dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
TempTuonoProject {
original_dir,
temp_dir,
}
}
pub fn add_file_with_content<'a>(&self, path: &'a str, content: &'a str) {
let path = PathBuf::from(path);
create_all(
path.parent().expect("File path does not have any parent"),
false,
)
.expect("Failed to create parent file directories");
let mut file = File::create(path).expect("Failed to create the file");
file.write_all(content.as_bytes())
.expect("Failed to write into the file");
}
}
impl Drop for TempTuonoProject {
fn drop(&mut self) {
// Set back the current dir in the previous state
env::set_current_dir(self.original_dir.to_owned())
.expect("Failed to restore the original directory.");
}
}
+19 -11
View File
@@ -1,11 +1,12 @@
[package]
name = "tuono_lib"
version = "0.8.2"
version = "0.17.8"
edition = "2021"
authors = ["V. Ageno <valerioageno@yahoo.it>"]
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
repository = "https://github.com/tuono-labs/tuono"
readme = "../../README.md"
license-file = "../../LICENSE.md"
categories = ["web-programming"]
@@ -14,13 +15,11 @@ include = [
"Cargo.toml"
]
[lib]
name = "tuono_lib"
path = "src/lib.rs"
[dependencies]
ssr_rs = "0.5.5"
ssr_rs = "0.8.1"
axum = {version = "0.7.5", features = ["json", "ws"]}
axum-extra = {version = "0.9.6", features = ["cookie"]}
tokio = { version = "1.37.0", features = ["full"] }
serde = { version = "1.0.202", features = ["derive"] }
erased-serde = "0.4.5"
@@ -30,11 +29,20 @@ reqwest = {version = "0.12.4", features = ["json", "stream"]}
once_cell = "1.19.0"
regex = "1.10.5"
either = "1.13.0"
tower-http = {version = "0.5.2", features = ["fs"]}
tower-http = {version = "0.6.0", features = ["fs"]}
colored = "2.1.0"
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.8.2"}
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.17.8"}
tuono_internal = {path = "../tuono_internal", version = "0.17.8"}
# Match the same version used by axum
tokio-tungstenite = "0.21.0"
tokio-tungstenite = "0.24.0"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
tungstenite = "0.23.0"
tungstenite = "0.24.0"
http = "1.1.0"
pin-project = "1.1.7"
tower = "0.5.1"
[dev-dependencies]
fs_extra = "1.3.0"
tempfile = "3.14.0"
serial_test = "0.10.0"
+4
View File
@@ -0,0 +1,4 @@
use once_cell::sync::OnceCell;
use tuono_internal::config::Config;
pub static GLOBAL_CONFIG: OnceCell<Config> = OnceCell::new();
+11 -3
View File
@@ -1,4 +1,11 @@
//! ## 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 config;
mod logger;
mod manifest;
mod mode;
mod payload;
@@ -13,10 +20,11 @@ pub use mode::Mode;
pub use payload::Payload;
pub use request::Request;
pub use response::{Props, Response};
pub use server::Server;
pub use tuono_lib_macros::handler;
pub use server::{tuono_internal_init_v8_platform, Server};
pub use tuono_lib_macros::{api, handler};
// Re-exports
pub use axum;
pub use reqwest;
pub use axum_extra::extract::cookie;
pub use ssr_rs::Ssr;
pub use tokio;
+106
View File
@@ -0,0 +1,106 @@
use colored::Colorize;
use http::{method::Method, Request, Response};
use pin_project::pin_project;
use std::fmt::Debug;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::time::Instant;
use tower::{Layer, Service};
#[derive(Clone)]
pub struct LoggerLayer {}
impl LoggerLayer {
pub fn new() -> Self {
LoggerLayer {}
}
}
impl<S> Layer<S> for LoggerLayer {
type Service = Logger<S>;
fn layer(&self, inner: S) -> Self::Service {
Logger::new(inner)
}
}
#[derive(Clone)]
pub struct Logger<S> {
inner: S,
}
impl<S> Logger<S> {
pub fn new(inner: S) -> Self {
Logger { inner }
}
}
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Logger<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
ResBody: Default,
<S as Service<Request<ReqBody>>>::Error: Debug,
{
type Response = S::Response;
type Error = S::Error;
type Future = LoggerFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let method = req.method().clone();
let path = req.uri().path().to_string();
LoggerFuture {
future: self.inner.call(req),
method,
path,
start: Instant::now(),
}
}
}
#[pin_project]
pub struct LoggerFuture<F> {
#[pin]
future: F,
method: Method,
path: String,
start: Instant,
}
impl<F, B, E> Future for LoggerFuture<F>
where
F: Future<Output = Result<Response<B>, E>>,
B: Default,
E: Debug,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res: F::Output = match this.future.poll(cx) {
Poll::Ready(res) => res,
Poll::Pending => return Poll::Pending,
};
if this.path.starts_with("/__tuono/data") {
return Poll::Ready(res);
}
let status_code = res.as_ref().unwrap().status();
println!(
" {} {} {} in {}ms",
this.method,
this.path,
status_code.as_str().green(),
this.start.elapsed().as_millis()
);
Poll::Ready(res)
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ pub fn load_manifest() {
let file = File::open(PathBuf::from(VITE_MANIFEST_PATH)).unwrap();
let reader = BufReader::new(file);
let manifest = serde_json::from_reader(reader).unwrap();
MANIFEST.set(remap_manifest_keys(manifest)).unwrap();
let _ = MANIFEST.set(remap_manifest_keys(manifest));
}
fn remap_manifest_keys(manifest: HashMap<String, BundleInfo>) -> HashMap<String, BundleInfo> {
+72 -10
View File
@@ -1,8 +1,10 @@
use crate::config::GLOBAL_CONFIG;
use crate::manifest::MANIFEST;
use crate::mode::{Mode, GLOBAL_MODE};
use erased_serde::Serialize;
use regex::Regex;
use serde::Serialize as SerdeSerialize;
use tuono_internal::config::ServerConfig;
use crate::request::{Location, Request};
@@ -14,23 +16,38 @@ fn has_dynamic_path(route: &str) -> bool {
#[derive(SerdeSerialize)]
/// This is the payload sent to the client for hydration
pub struct Payload<'a> {
router: Location,
props: &'a dyn Serialize,
location: Location,
data: &'a dyn Serialize,
mode: Mode,
#[serde(rename(serialize = "jsBundles"))]
js_bundles: Option<Vec<&'a String>>,
#[serde(rename(serialize = "cssBundles"))]
css_bundles: Option<Vec<&'a String>>,
#[serde(rename(serialize = "devServerConfig"))]
dev_server_config: Option<&'a ServerConfig>,
}
impl<'a> Payload<'a> {
pub fn new(req: &'a Request, props: &'a dyn Serialize) -> Payload<'a> {
pub fn new(req: &'a Request, data: &'a dyn Serialize) -> Payload<'a> {
let config = GLOBAL_CONFIG
.get()
.expect("Failed to load the current config");
let mode = *GLOBAL_MODE.get().expect("Failed to load the current mode");
let dev_server_config = if mode == Mode::Dev {
Some(&config.server)
} else {
None
};
Payload {
router: req.location(),
props,
mode: *GLOBAL_MODE.get().expect("Failed to load the current mode"),
location: req.location(),
data,
mode,
js_bundles: None,
css_bundles: None,
dev_server_config,
}
}
@@ -62,7 +79,7 @@ impl<'a> Payload<'a> {
let mut js_bundles_sources = vec![&main_bundle.file];
let mut css_bundles_sources = main_bundle.css.iter().collect::<Vec<&String>>();
let pathname = &self.router.pathname();
let pathname = &self.location.pathname();
let bundle_data = manifest.get(*pathname);
@@ -84,7 +101,7 @@ impl<'a> Payload<'a> {
.filter(|path| !path.is_empty())
.collect::<Vec<&str>>();
for dyn_route in dynamic_routes.iter() {
'_dynamic_routes_loop: for dyn_route in dynamic_routes.iter() {
let dyn_route_segments = dyn_route
.split('/')
.filter(|path| !path.is_empty())
@@ -93,6 +110,20 @@ impl<'a> Payload<'a> {
let mut route_segments_collector: Vec<&str> = vec![];
for i in 0..dyn_route_segments.len() {
if dyn_route_segments[i].starts_with("[...") {
route_segments_collector.push(dyn_route_segments[i]);
let manifest_key = route_segments_collector.join("/");
let route_data = manifest.get(&format!("/{manifest_key}"));
if let Some(data) = route_data {
js_bundles_sources.push(&data.file);
data.css
.iter()
.for_each(|source| css_bundles_sources.push(source))
}
break '_dynamic_routes_loop;
}
if path_segments.len() == i {
break;
}
@@ -168,6 +199,14 @@ mod tests {
css: vec!["assets/posts/[post]/[comment].css".to_string()],
},
);
manifest_mock.insert(
"/pokemons/[...catch_all]".to_string(),
BundleInfo {
file: "assets/catch_all.js".to_string(),
css: vec!["assets/catch_all.css".to_string()],
},
);
manifest_mock.insert(
"/posts/custom-post".to_string(),
BundleInfo {
@@ -194,11 +233,12 @@ mod tests {
let location = Location::from(uri);
Payload {
router: location,
props: &None::<Option<()>>,
location,
data: &None::<Option<()>>,
mode,
js_bundles: None,
css_bundles: None,
dev_server_config: None,
}
}
@@ -274,6 +314,28 @@ mod tests {
);
}
#[test]
fn should_load_the_correct_catch_all_bundles() {
let mut payload = prepare_payload(
Some("http://localhost:3000/pokemons/a-poke/a-poke"),
Mode::Prod,
);
let _ = payload.client_payload();
assert_eq!(
payload.js_bundles,
Some(vec![
&"assets/bundled-file.js".to_string(),
&"assets/catch_all.js".to_string()
])
);
assert_eq!(
payload.css_bundles,
Some(vec![
&"assets/bundled-file.css".to_string(),
&"assets/catch_all.css".to_string()
])
);
}
#[test]
fn should_load_the_defined_path_bundles() {
let mut payload = prepare_payload(Some("http://localhost:3000/about"), Mode::Prod);
+1 -1
View File
@@ -34,7 +34,7 @@ impl From<Uri> for Location {
#[derive(Debug, Clone)]
pub struct Request {
uri: Uri,
pub uri: Uri,
pub headers: HeaderMap,
pub params: HashMap<String, String>,
}
+74 -12
View File
@@ -1,18 +1,22 @@
use crate::Request;
use crate::{ssr::Js, Payload};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Redirect, Response as AxumResponse};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect};
use axum::Json;
use axum_extra::extract::cookie::{Cookie, CookieJar};
use erased_serde::Serialize;
pub struct Props {
data: Box<dyn Serialize>,
http_code: StatusCode,
cookies: CookieJar,
}
pub enum Response {
Redirect(String),
Props(Props),
// TODO: improve this tuple to support a more generic IntoResponse
Custom((StatusCode, HeaderMap, String)),
}
#[derive(serde::Serialize)]
@@ -55,44 +59,102 @@ impl Props {
Props {
data: Box::new(data),
http_code: StatusCode::OK,
cookies: CookieJar::new(),
}
}
pub fn status(&mut self, http_code: StatusCode) {
self.http_code = http_code;
}
pub fn new_with_status(data: impl Serialize + 'static, http_code: StatusCode) -> Self {
Props {
data: Box::new(data),
http_code,
cookies: CookieJar::new(),
}
}
pub fn add_cookie(&mut self, cookie: Cookie) {
let jar = self.cookies.clone().add(cookie.into_owned());
self.cookies = jar
}
}
impl Response {
pub fn render_to_string(&self, req: Request) -> AxumResponse {
pub fn render_to_string(&self, req: Request) -> impl IntoResponse {
match self {
Self::Props(Props { data, http_code }) => {
let payload = Payload::new(&req, data).client_payload().unwrap();
Self::Props(Props {
data,
http_code,
cookies,
}) => {
let payload = Payload::new(&req, data.as_ref()).client_payload().unwrap();
match Js::render_to_string(Some(&payload)) {
Ok(html) => (*http_code, Html(html)).into_response(),
Err(_) => {
(*http_code, Html("500 Internal server error".to_string())).into_response()
}
Ok(html) => (*http_code, cookies.clone(), Html(html)),
Err(_) => (
*http_code,
cookies.clone(),
Html("500 Internal server error".to_string()),
),
}
.into_response()
}
Self::Redirect(to) => Redirect::permanent(to).into_response(),
Self::Custom(response) => response.clone().into_response(),
}
}
pub fn json(&self) -> impl IntoResponse {
match self {
Self::Props(Props { data, http_code }) => {
(*http_code, Json(JsonResponse::new(data))).into_response()
}
Self::Props(Props {
data,
http_code,
cookies,
}) => (
*http_code,
cookies.clone(),
Json(JsonResponse::new(data.as_ref())),
)
.into_response(),
Self::Redirect(destination) => (
StatusCode::PERMANENT_REDIRECT,
Json(JsonResponse::new_redirect(destination.to_string())),
)
.into_response(),
// Custom never needs the "data" response since its scope
// is outside the react domain
Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_update_the_props_status_and_cookie() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
#[test]
fn should_add_a_cookie_jar() {
let mut props = Props::new("{}");
props.status(StatusCode::NOT_FOUND);
props.add_cookie(Cookie::new("test", "cookie"));
assert_eq!(props.http_code, StatusCode::NOT_FOUND);
assert_eq!(
props.cookies.get("test").unwrap(),
&Cookie::new("test", "cookie")
);
}
}
+50 -20
View File
@@ -1,63 +1,93 @@
use crate::mode::{Mode, GLOBAL_MODE};
use crate::config::GLOBAL_CONFIG;
use crate::manifest::load_manifest;
use crate::mode::{Mode, GLOBAL_MODE};
use axum::routing::{get, Router};
use colored::Colorize;
use ssr_rs::Ssr;
use tower_http::services::ServeDir;
use tuono_internal::config::Config;
use crate::{
catch_all::catch_all, vite_reverse_proxy::vite_reverse_proxy,
catch_all::catch_all, logger::LoggerLayer, vite_reverse_proxy::vite_reverse_proxy,
vite_websocket_proxy::vite_websocket_proxy,
};
const DEV_PUBLIC_DIR: &str = "public";
const PROD_PUBLIC_DIR: &str = "out/client";
pub fn tuono_internal_init_v8_platform() {
Ssr::create_platform();
}
#[derive(Debug)]
pub struct Server {
router: Router<reqwest::Client>,
router: Router,
mode: Mode,
pub listener: tokio::net::TcpListener,
pub address: String,
}
impl Server {
pub fn init(router: Router<reqwest::Client>, mode: Mode) -> Server {
Ssr::create_platform();
pub async fn init(router: Router, mode: Mode) -> Server {
let config = Config::get().expect("[SERVER] Failed to load config");
GLOBAL_MODE.set(mode).unwrap();
let _ = GLOBAL_MODE.set(mode);
let _ = GLOBAL_CONFIG.set(config.clone());
if mode == Mode::Prod {
load_manifest()
}
Server { router, mode }
let server_address = format!("{}:{}", config.server.host, config.server.port);
Server {
router,
mode,
address: server_address.clone(),
listener: tokio::net::TcpListener::bind(&server_address)
.await
.expect("[SERVER] Failed to bind to address"),
}
}
pub async fn start(&self) {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
let fetch = reqwest::Client::new();
pub async fn start(self) {
/*
* Format the server address as a valid URL so that it becomes clickable in the CLI
* @see https://github.com/tuono-labs/tuono/issues/460
*/
let server_base_url = format!("http://{}", self.address);
if self.mode == Mode::Dev {
println!("\nDevelopment app ready at http://localhost:3000/");
println!(" Ready at: {}\n", server_base_url.blue().bold());
let router = self
.router
.to_owned()
.layer(LoggerLayer::new())
.route("/vite-server/", get(vite_websocket_proxy))
.route("/vite-server/*path", get(vite_reverse_proxy))
.fallback_service(ServeDir::new(DEV_PUBLIC_DIR).fallback(get(catch_all)))
.with_state(fetch);
.fallback_service(
ServeDir::new(DEV_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
);
axum::serve(listener, router)
axum::serve(self.listener, router)
.await
.expect("Failed to serve development server");
} else {
println!("\nProduction app ready at http://localhost:3000/");
println!(
" Production server at: {}\n",
server_base_url.blue().bold()
);
let router = self
.router
.to_owned()
.fallback_service(ServeDir::new(PROD_PUBLIC_DIR).fallback(get(catch_all)))
.with_state(fetch);
.layer(LoggerLayer::new())
.fallback_service(
ServeDir::new(PROD_PUBLIC_DIR)
.fallback(get(catch_all).layer(LoggerLayer::new())),
);
axum::serve(listener, router)
axum::serve(self.listener, router)
.await
.expect("Failed to serve production server");
}
+15 -6
View File
@@ -1,5 +1,5 @@
use crate::mode::{Mode, GLOBAL_MODE};
use ssr_rs::Ssr;
use ssr_rs::{Ssr, SsrError};
use std::cell::RefCell;
use std::fs::read_to_string;
use std::path::PathBuf;
@@ -9,8 +9,18 @@ use std::path::PathBuf;
/// update the SSR result without reloading the whole server.
pub struct Js;
#[cfg(target_os = "windows")]
const PROD_BUNDLE_PATH: &str = ".\\out\\server\\prod-server.js";
#[cfg(target_os = "windows")]
const DEV_BUNDLE_PATH: &str = ".\\.tuono\\server\\dev-server.js";
#[cfg(not(target_os = "windows"))]
const PROD_BUNDLE_PATH: &str = "./out/server/prod-server.js";
#[cfg(not(target_os = "windows"))]
const DEV_BUNDLE_PATH: &str = "./.tuono/server/dev-server.js";
impl Js {
pub fn render_to_string(payload: Option<&str>) -> Result<String, &'static str> {
pub fn render_to_string(payload: Option<&str>) -> Result<String, SsrError> {
let mode = GLOBAL_MODE.get().expect("Failed to get GLOBAL_MODE");
if *mode == Mode::Dev {
@@ -27,7 +37,7 @@ impl ProdJs {
thread_local! {
pub static SSR: RefCell<Ssr<'static, 'static>> = RefCell::new(
Ssr::from(
read_to_string(PathBuf::from("./out/server/prod-server.js")).expect("Server bundle not found"), ""
read_to_string(PathBuf::from(PROD_BUNDLE_PATH)).expect("Server bundle not found"), ""
).unwrap()
)
}
@@ -36,10 +46,9 @@ impl ProdJs {
struct DevJs;
impl DevJs {
pub fn render_to_string(params: Option<&str>) -> Result<String, &'static str> {
pub fn render_to_string(params: Option<&str>) -> Result<String, SsrError> {
Ssr::from(
read_to_string(PathBuf::from("./.tuono/server/dev-server.js"))
.expect("Server bundle not found"),
read_to_string(PathBuf::from(DEV_BUNDLE_PATH)).expect("Server bundle not found"),
"",
)
.unwrap()
+15 -7
View File
@@ -1,17 +1,25 @@
use crate::config::GLOBAL_CONFIG;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::extract::Path;
use axum::http::{HeaderName, HeaderValue};
use axum::response::{IntoResponse, Response};
use reqwest::Client;
const VITE_URL: &str = "http://localhost:3001/vite-server";
pub async fn vite_reverse_proxy(Path(path): Path<String>) -> impl IntoResponse {
let client = Client::new();
pub async fn vite_reverse_proxy(
State(client): State<Client>,
Path(path): Path<String>,
) -> impl IntoResponse {
match client.get(format!("{VITE_URL}/{path}")).send().await {
let config = GLOBAL_CONFIG
.get()
.expect("Failed to get the internal config");
let vite_url = format!(
"http://{}:{}/vite-server",
config.server.host,
config.server.port + 1
);
match client.get(format!("{vite_url}/{path}")).send().await {
Ok(res) => {
let mut response_builder = Response::builder().status(res.status().as_u16());
+12 -2
View File
@@ -1,3 +1,4 @@
use crate::config::GLOBAL_CONFIG;
use axum::extract::ws::{self, WebSocket, WebSocketUpgrade};
use axum::response::IntoResponse;
use futures_util::{SinkExt, StreamExt};
@@ -6,7 +7,6 @@ use tokio_tungstenite::tungstenite::{Error, Message};
use tungstenite::client::IntoClientRequest;
use tungstenite::ClientRequestBuilder;
const VITE_WS: &str = "ws://localhost:3001/vite-server/";
const VITE_WS_PROTOCOL: &str = "vite-hmr";
/// This is the entry point to proxy the vite's WebSocket.
@@ -28,7 +28,17 @@ async fn handle_socket(mut tuono_socket: WebSocket) {
return;
}
let vite_ws_request = ClientRequestBuilder::new(VITE_WS.parse().unwrap())
let config = GLOBAL_CONFIG
.get()
.expect("Failed to get the internal config");
let vite_ws = format!(
"ws://{}:{}/vite-server/",
config.server.host,
config.server.port + 1
);
let vite_ws_request = ClientRequestBuilder::new(vite_ws.parse().unwrap())
.with_sub_protocol(VITE_WS_PROTOCOL)
.into_client_request()
.expect("Failed to create vite WS request");
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
pub mod utils;
+105
View File
@@ -0,0 +1,105 @@
mod utils;
use crate::utils::utils::MockTuonoServer;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn api_endpoint_work() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let response = client
.get(&format!("{server_url}/health_check"))
.send()
.await
.expect("Failed to execute request.");
// Assert
assert!(response.status().is_success());
assert_eq!(Some(0), response.content_length());
}
#[tokio::test]
#[serial]
async fn not_found_route() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let response = client
.get(&format!("{server_url}/not-found"))
.send()
.await
.expect("Failed to execute request.");
// TODO: This should return a 404 status code
assert!(response.status().is_success());
assert_eq!(
response.text().await.unwrap(),
"<h1>404 Not found</h1><a href=\"/\">Return home</a>"
);
}
#[tokio::test]
#[serial]
async fn index_html_route() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let response = client
.get(&format!("{server_url}/"))
.send()
.await
.expect("Failed to execute request.");
// TODO: This should return a 404 status code
assert!(response.status().is_success());
assert!(response
.text()
.await
.unwrap()
.starts_with("<!DOCTYPE html>"));
}
#[tokio::test]
#[serial]
async fn api_route_route() {
let app = MockTuonoServer::spawn().await;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let server_url = format!("http://{}:{}", &app.address, &app.port);
let response = client
.get(&format!("{server_url}/tuono/data"))
.send()
.await
.expect("Failed to execute request.");
// TODO: This should return a 404 status code
assert!(response.status().is_success());
assert_eq!(
response.text().await.unwrap(),
"{\"data\":\"{}\",\"info\":{\"redirect_destination\":null}}"
);
}
@@ -0,0 +1,7 @@
use tuono_lib::axum::http::StatusCode;
use tuono_lib::Request;
#[tuono_lib::api(GET)]
async fn health_check(_req: Request) -> StatusCode {
StatusCode::OK
}
+3
View File
@@ -0,0 +1,3 @@
pub mod health_check;
pub mod route;
pub mod utils;
+6
View File
@@ -0,0 +1,6 @@
use tuono_lib::{Props, Request, Response};
#[tuono_lib::handler]
async fn route(_: Request) -> Response {
Response::Props(Props::new("{}"))
}
+106
View File
@@ -0,0 +1,106 @@
use fs_extra::dir::create_all;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::{env, fs};
use tempfile::{tempdir, TempDir};
use tuono_lib::axum::routing::get;
use tuono_lib::{axum::Router, tuono_internal_init_v8_platform, Mode, Server};
use crate::utils::health_check::get__tuono_internal_api as health_check;
use crate::utils::route as html_route;
use crate::utils::route::tuono__internal__api as route_api;
use std::sync::Once;
static INIT: Once = Once::new();
fn init_v8() {
INIT.call_once(|| {
tuono_internal_init_v8_platform();
})
}
fn add_file_with_content<'a>(path: &'a str, content: &'a str) {
let path = PathBuf::from(path);
create_all(
path.parent().expect("File path does not have any parent"),
false,
)
.expect(
format!(
"Failed to create parent file directories: {}",
path.display()
)
.as_str(),
);
let mut file = File::create(path).expect("Failed to create the file");
file.write_all(content.as_bytes())
.expect("Failed to write into the file");
}
#[derive(Debug)]
pub struct MockTuonoServer {
pub address: String,
pub port: u16,
original_dir: PathBuf,
#[allow(dead_code)]
// Required for dropping the temp_dir when this struct drops
temp_dir: TempDir,
}
impl MockTuonoServer {
pub async fn spawn() -> Self {
init_v8();
let original_dir = env::current_dir().expect("Failed to read current_dir");
let temp_dir = tempdir().expect("Failed to create temp_dir");
let react_prod_build = fs::read_to_string("./tests/assets/fake_react_build.js")
.expect("Failed to read fake_react_build.js");
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
add_file_with_content(
"./.tuono/config/config.json",
r#"{"server": {"host": "127.0.0.1", "port": 0}}"#,
);
add_file_with_content("./out/server/prod-server.js", react_prod_build.as_str());
add_file_with_content(
"./out/client/.vite/manifest.json",
r#"{"client-main.tsx": { "file": "assets/index.js", "name": "index", "src": "index.tsx", "isEntry": true,"dynamicImports": [],"css": []}}"#,
);
let router = Router::new()
.route("/", get(html_route::tuono__internal__route))
.route("/tuono/data", get(html_route::tuono__internal__api))
.route("/health_check", get(health_check))
.route("/route-api", get(route_api));
let server = Server::init(router, Mode::Prod).await;
let socket = server
.listener
.local_addr()
.expect("Failed to extract test server socket");
let _ = tokio::spawn(server.start());
MockTuonoServer {
address: socket.ip().to_string(),
port: socket.port(),
original_dir,
temp_dir,
}
}
}
impl Drop for MockTuonoServer {
fn drop(&mut self) {
// Set back the current dir in the previous state
env::set_current_dir(self.original_dir.to_owned())
.expect("Failed to restore the original directory.");
}
}
+6 -6
View File
@@ -1,10 +1,11 @@
[package]
name = "tuono_lib_macros"
version = "0.8.2"
version = "0.17.8"
edition = "2021"
description = "The react/rust fullstack framework"
description = "Superfast React fullstack framework"
homepage = "https://tuono.dev"
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
repository = "https://github.com/Valerioageno/tuono"
repository = "https://github.com/tuono-labs/tuono"
readme = "../../README.md"
license-file = "../../LICENSE.md"
categories = ["web-programming"]
@@ -14,9 +15,8 @@ include = [
]
[lib]
crate_type = ["proc-macro"]
proc-marco = true
proc-macro = true
[dependencies]
syn = { version = "1.0.59", features = ["full"] }
syn = { version = "2.0.0", features = ["full"] }
quote = "1.0"
+69
View File
@@ -0,0 +1,69 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::{Span, TokenStream};
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, FnArg, Ident, ItemFn, Pat};
pub fn api_core(attrs: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let http_method = parse_macro_input!(attrs as Ident)
.to_string()
.to_lowercase();
let api_fn_name = Ident::new(
&format!("{}__tuono_internal_api", http_method),
Span::call_site().into(),
);
let fn_name = &item.sig.ident;
let return_type = &item.sig.output;
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
// Fn Arguments minus the first which always is the request
for (i, arg) in item.sig.inputs.iter().enumerate() {
if i == 0 {
axum_arguments.insert(i, params_argument());
continue;
}
if i == 1 {
axum_arguments.insert(1, create_struct_fn_arg())
}
if let FnArg::Typed(pat_type) = arg {
let index = i - 1;
let argument_name = *pat_type.pat.clone();
argument_names.insert(index, argument_name.clone());
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
let application_state_import = import_main_application_state(argument_names.clone());
quote! {
#application_state_import
#item
pub async fn #api_fn_name(#axum_arguments)#return_type {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), #argument_names).await
}
}
.into()
}
+50 -17
View File
@@ -1,43 +1,76 @@
use crate::utils::{
crate_application_state_extractor, create_struct_fn_arg, import_main_application_state,
params_argument, request_argument,
};
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_macro_input, FnArg, ItemFn, Pat};
pub fn handler_core(_args: TokenStream, item: TokenStream) -> TokenStream {
let item = parse_macro_input!(item as ItemFn);
let fn_name = item.clone().sig.ident;
let fn_name = &item.sig.ident;
let mut argument_names: Punctuated<Pat, Comma> = Punctuated::new();
let mut axum_arguments: Punctuated<FnArg, Comma> = Punctuated::new();
// Fn Arguments minus the first which always is the request
for (i, arg) in item.sig.inputs.iter().enumerate() {
if i == 0 {
axum_arguments.insert(i, params_argument());
continue;
}
if i == 1 {
axum_arguments.insert(1, create_struct_fn_arg())
}
if let FnArg::Typed(pat_type) = arg {
let index = i - 1;
let argument_name = *pat_type.pat.clone();
argument_names.insert(index, argument_name.clone());
}
}
axum_arguments.insert(axum_arguments.len(), request_argument());
let application_state_extractor = crate_application_state_extractor(argument_names.clone());
let application_state_import = import_main_application_state(argument_names.clone());
quote! {
use tuono_lib::axum::response::IntoResponse;
use std::collections::HashMap;
use tuono_lib::axum::extract::{State, Path};
#application_state_import
#item
pub async fn route(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse {
pub async fn tuono__internal__route(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.render_to_string(req)
#fn_name(req.clone(), #argument_names).await.render_to_string(req)
}
pub async fn api(
Path(params): Path<HashMap<String, String>>,
State(client): State<tuono_lib::reqwest::Client>,
request: tuono_lib::axum::extract::Request
) -> impl IntoResponse{
pub async fn tuono__internal__api(
#axum_arguments
) -> impl tuono_lib::axum::response::IntoResponse {
#application_state_extractor
let pathname = request.uri();
let headers = request.headers();
let req = tuono_lib::Request::new(pathname.to_owned(), headers.to_owned(), params);
#fn_name(req.clone(), client).await.json()
#fn_name(req.clone(), #argument_names).await.json()
}
}
.into()
+12
View File
@@ -1,9 +1,21 @@
//! ## 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;
mod api;
mod handler;
mod utils;
#[proc_macro_attribute]
pub fn handler(args: TokenStream, item: TokenStream) -> TokenStream {
handler::handler_core(args, item)
}
#[proc_macro_attribute]
pub fn api(args: TokenStream, item: TokenStream) -> TokenStream {
api::api_core(args, item)
}
+47
View File
@@ -0,0 +1,47 @@
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse2, parse_quote, FnArg, Pat, Stmt};
pub fn create_struct_fn_arg() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::State(state): tuono_lib::axum::extract::State<ApplicationState>
})
.unwrap()
}
pub fn import_main_application_state(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let local: Stmt = parse_quote!(
use crate::tuono_main_state::ApplicationState;
);
return Some(local);
}
None
}
pub fn crate_application_state_extractor(argument_names: Punctuated<Pat, Comma>) -> Option<Stmt> {
if !argument_names.is_empty() {
let use_item: Stmt = parse_quote!(let ApplicationState { #argument_names, .. } = state;);
return Some(use_item);
}
None
}
pub fn params_argument() -> FnArg {
parse2(quote! {
tuono_lib::axum::extract::Path(params): tuono_lib::axum::extract::Path<
std::collections::HashMap<String, String>
>
})
.unwrap()
}
pub fn request_argument() -> FnArg {
parse2(quote! {
request: tuono_lib::axum::extract::Request
})
.unwrap()
}
+19
View File
@@ -0,0 +1,19 @@
# project
node_modules
.pnpm-store
target
.turbo
# git
.git
.gitignore
# docker
docker/compose.yml
docker/Dockerfile
docker/.dockerignore
# IDE
.vscode
.idea
*.iml
+51
View File
@@ -0,0 +1,51 @@
# ARGs obtained from the compose.yml file
ARG BASE_OS=bookworm
ARG RUST_VERSION=1.83
FROM rust:${RUST_VERSION}-${BASE_OS}
# Nodejs installation
RUN ["mkdir", "/usr/local/node"]
WORKDIR /usr/local/node
## Retrieves the targeted version of Node in the container
COPY .nvmrc /tmp/.nvmrc
## Download the Node.js standelone binary archive && extract it && clean files
RUN ["/bin/sh", "-c", "NODE_VERSION=$(cat /tmp/.nvmrc | tr -d '\r') && \
wget https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz && \
tar xf node-v${NODE_VERSION}-linux-x64.tar.xz --strip-components 1 && \
rm node-v${NODE_VERSION}-linux-x64.tar.xz /tmp/.nvmrc"]
## Ensure Node.js binaries are available in the PATH
ENV PATH /usr/local/node/bin:$PATH
# cargo-watch installation (for Rust live-reloading during dev')
RUN ["cargo", "install", "cargo-watch"]
# global configuration done ! let's focus on the project itself
## Copy project files into the container (excepted files/dir declared in .dockerignore) to /tuono directory
WORKDIR /tuono
COPY . .
# pnpm installation based on version found in package.json
RUN ["corepack", "enable"]
RUN ["corepack", "install"]
## Install Node.js dependencies (defined in package.json)
RUN ["pnpm", "install", "--frozen-lockfile"]
## Compile and add the directory with compiled binaries to the PATH for direct access
RUN ["cargo", "build"]
ENV PATH=/tuono/target/debug:$PATH
+27
View File
@@ -0,0 +1,27 @@
services:
tuono:
platform: linux/amd64
container_name: tuono-source-container
hostname: tuono-source-container
build:
context: ../
dockerfile: ./docker/Dockerfile
args:
RUST_VERSION: 1.83 # if change, think to update the default ARGs in Dockerfile
BASE_OS: bookworm # if change, think to update the default ARGs in Dockerfile
image: tuono-source-image
ports:
- '3000:3000' # expose default port used by tuono server
volumes: # mount the project into a volume but node_modules / .pnpm-store / target / ... directories won't by sync from container to host
- ..:/tuono
- /tuono/node_modules
- /tuono/target
- /tuono/.pnpm-store
- /tuono/.turbo
- /tuono/examples/tuono-app/node_modules
- /tuono/examples/tuono-app/target
- /tuono/examples/tuono-tutorial/node_modules
- /tuono/examples/tuono-tutorial/target
- /tuono/examples/with-mdx/node_modules
- /tuono/examples/with-mdx/target
tty: true
-12
View File
@@ -1,12 +0,0 @@
# Documentation
This folder contains the documentation related to the `tuono` framework.
The framework expects to enable SSG and MDX support in the next months.
At that time a standalone website will be built with tuono to hold the whole documentation.
The following docs are currently actively maintained:
- [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md)
## Contributions
Any help or suggestion will be strongly appreciated.
-697
View File
@@ -1,697 +0,0 @@
# Tuono tutorial
This tutorial is meant for giving you a sneak peek of the framework and is intended to evolve along the development - be sure to have installed the latest version.
The first part is about the project setup and the base knowledge needed to work with tuono. The actual tutorial starts at [Tutorial introduction](#tutorial-introduction).
> I'd love to hear your thoughts about the framework and the tutorial - feel free to reach me
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or on Twitter (X) DMs
[@valerioageno](https://twitter.com/valerioageno)
This tutorial is **not meant** for people that don't know React - in that case I suggest you to first read the [React doc](https://react.dev/);
Typescript and Rust knowledge is not a requirement though!
## Table of Content
* [CLI Installation](#cli-installation)
* [Project scaffold](#project-scaffold)
* [Start the dev environment](#start-the-dev-environment)
* [The “/” route](#the--route)
* [Tutorial introduction](#tutorial-introduction)
* [Fetch all the pokemons](#fetch-all-the-pokemons)
* [Create a stand-alone component](#create-a-stand-alone-component)
* [Create the /pokemons/[pokemon] route](#create-the-pokemonspokemon-route)
* [Error handling](#error-handling)
* [Seo and meta tags](#seo-and-meta-tags)
* [Handle redirections](#handle-redirections)
* [Building for production](#building-for-production)
* [Conclusion](#conclusion)
## CLI Installation
The tuono CLI is hosted on [crates.io](https://crates.io/crates/tuono); to download and install it just run on a terminal:
```bash
$ cargo install tuono
```
To check that is correctly installed run:
```bash
$ tuono --version
```
Run `tuono -h` to see all the available commands.
## Project scaffold
To setup a new fresh project you just need to run the following command:
```bash
$ tuono new tuono-tutorial
```
Get into the project folder and install the dependencies with:
```bash
$ npm install
```
Open it with your favourite code editor.
The project will have the following structure:
```
├── package.json
├── public
├── src
│ ├── routes
│ └── styles
├── Cargo.toml
├── README.md
└── tsconfig.json
```
**public/**: put here all the files you want to be public
**src/routes/**: All the files in this folder are considered routes. All the routes are server side rendered out of the box. To add server side capabilities just create a rust file with the same name as the route (i.e. `about.tsx``about.rs`).
**src/styles/**: In this folder there is the `global.css` file that stores all the global styles. For the rest of the project you can use CSS modules (⚠️ CSS modules on routes are forbidden).
## Start the dev environment
To start the development environment you just need to run the following command within the project folder:
```bash
$ tuono dev
```
The first time might take a little bit because it will install all the rusts dependencies. All the other execution will be pretty quick!
> 💡 The `tuono dev` development script is currently under strong optimization improvements. In case you face any error delete the cache `.tuono` folder and run it again!
Then open [`http://localhost:3000/`](http://localhost:3000/) on the browser.
## The “/” route
All the `index.tsx` files represent the folder root page (i.e. `src/routes/posts/index.tsx` is [`http://localhost:3000/posts`](http://localhost:3000/posts) as well as `src/routes/posts.tsx`).
The file `index.rs` represents the server side capabilities for the index route. On this file you can:
- Passing server side props
- Changing http status code
- Redirecting to a different route
## Tutorial introduction
Now that we have some knowledge about the project structure lets start the real tutorial.
The goal is to use the [PokeAPI](https://pokeapi.co/docs/v2) to list all the pokemons of the first generation (the best one btw) and then reserve a dynamic page for each one separately.
> If you have already installed the tuono CLI and you prefer read the code instead of writing it yourself
you can download the tutorial source with `tuono new tuono-tutorial --template tutorial`
## Fetch all the pokemons
To start lets fetch all of them in the root page; since we want to render them on the server side we gonna need to implement the logic in the `index.rs` file.
Clear the `index.rs` file and paste:
```rust
// src/routes/index.rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemons {
results: Vec<Pokemon>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
url: String,
}
#[tuono_lib::handler]
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap();
Response::Props(Props::new(data))
}
Err(_err) => Response::Props(Props::new("{}")),
};
}
```
Now the pokemons are correctly fetched and hydrated on the client side so we can actually use them. Clear the `index.tsx` file and paste:
```tsx
// src/routes/index.tsx
import type { TuonoProps } from "tuono";
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>;
}
return (
<>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
{data.results.map((pokemon) => {
return pokemon.name;
})}
</ul>
</>
);
}
```
Refresh now the browser! A bit ugly but all the pokemons are finally printed on screen!
## Create a stand-alone component
Lets then create the button needed for displaying the list of pokemons.
Create the following file `src/components/PokemonLink.tsx` and fill the content with:
```tsx
// src/components/PokemonLink.tsx
import { Link } from "tuono";
interface Pokemon {
name: string
}
export default function PokemonLink({
pokemon,
id,
}: {
pokemon: Pokemon;
id: number;
}): JSX.Element {
return (
<Link href={`/pokemons/${pokemon.name}`}>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
/>
</Link>
);
}
```
Now that the link is done lets import it in the `index.tsx` file
```diff
// src/routes/index.tsx
++ import PokemonLink from '../components/PokemonLink'
// ...
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
-- {pokemons.map((pokemon) => {
-- return pokemon.name;
++ {pokemons.map((pokemon, i) => {
++ return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />;
})}
</ul>
// ...
```
Now the links work. Clicking on any of them we get redirected to the 404 page because we havent yet implemented the `pokemons/[pokemon]` page.
As previously said CSS modules are enabled out of the box so lets make those links a little bit nicer.
Create alongside the `PokemonLink.tsx` component the CSS module `PokemonLink.module.css` and copy the following content into it:
```css
/* src/components/PokemonLink.module.css */
.link {
width: 100%;
max-width: 216px;
position: relative;
background: white;
margin-bottom: 10px;
border: solid #f0f0f0 1px;
text-decoration: none;
color: black;
padding: 5px 5px 5px 15px;
border-radius: 10px;
display: flex;
justify-content: space-between;
transition: 0.2s;
align-items: center;
}
.link:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.link img {
width: 70px;
background: white;
border-radius: 50%;
}
```
> 💡 SASS is supported out of the box. Just install the processor in the devDependencies `npm i -D sass` and run again `tuono dev`
Then import the styles into the `PokemonLink` component as following:
```diff
// src/components/PokemonLink.tsx
import { Link } from "tuono";
import type { Pokemon } from "./../types/pokemon";
++ import styles from './PokemonLink.module.css'
export default function PokemonLink({
pokemon,
id,
}: {
pokemon: Pokemon;
id: number;
}): JSX.Element {
return (
-- <Link href={`/pokemons/${pokemon.name}`}>
++ <Link className={styles.link} href={`/pokemons/${pokemon.name}`}>
{pokemon.name}
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
/>
</Link>
);
}
```
## Create the `/pokemons/[pokemon]` route
The homepage is ready. We have the full list of pokemons and they are all links. Great!
Now we want to make those links actually pointing to a real page. Lets create the dynamic route.
Create the folder `routes/pokemons` and then create the two files `[pokemon].tsx` and `[pokemon].rs`.
These two will handle every requests that points to `http://localhost:3000/pokemons/bulbasaur..mew`.
Lets first work on the server side file. Paste into the new `[pokemon].rs` file the following code:
```rust
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
use tuono_lib::reqwest::Client;
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
id: u16,
weight: u16,
height: u16,
}
#[tuono_lib::handler]
async fn get_pokemon(req: Request, fetch: Client) -> Response {
// The param `pokemon` is defined in the route filename [pokemon].rs
let pokemon = req.params.get("pokemon").unwrap();
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => {
let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Props::new(data))
}
Err(_err) => Response::Props(Props::new("{}"))
};
}
```
Then lets work on the frontend. Paste into the `[pokemon].tsx` file the following code:
```tsx
import { TuonoProps } from "tuono";
import PokemonView from "../../components/PokemonView";
export default function Pokemon({ data }: TuonoProps): JSX.Element {
return <PokemonView pokemon={data} />;
}
```
The browser should complain that the component `PokemonView` does not exist. Lets create it then!
```tsx
// components/PokemonView.tsx
import { Link } from "tuono";
import styles from "./PokemonView.module.css";
interface Pokemon {
name: string
id: string
weight: number
height: number
}
export default function PokemonView({
pokemon,
}: {
pokemon?: Pokemon;
}): JSX.Element {
return (
<div>
<Link className={styles["back-btn"]} href="/">
Back
</Link>
{pokemon?.name && (
<div className={styles.pokemon}>
<div>
<h1 className={styles.name}>
{pokemon.name}
</h1>
<dl className={styles.spec}>
<dt className={styles.label}>Weight:</dt>
<dd>{pokemon.weight}lbs</dd>
</dl>
<dl className={styles.spec}>
<dt className={styles.label}>Height:</dt>
<dd>{pokemon.height}ft</dd>
</dl>
</div>
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${pokemon.id}.png`}
/>
</div>
)}
</div>
);
}
```
```css
/* components/PokemonView.module.css */
.back-btn {
background-color: white;
border-radius: 10px;
padding: 7px 15px;
color: black;
text-decoration: none;
border: solid #f0f0f0 1px;
font-size: 20px;
}
.back-btn:hover {
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
}
.pokemon {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.name {
font-size: 50px;
font-weight: 700;
}
.pokemon img {
width: 400px;
}
.spec {
display: flex;
font-size: 18px;
margin-top: 10px;
}
.label {
font-weight: 700;
}
```
## Error handling
With the current setup all the routes always return a `200 Success` http status no matter the response type.
In order to return a more meaningful status code to the browser the `Props` struct can be initialized with also the
`Props::new_with_status()` method.
Let's see how it works!
```diff
// src/routes/pokemons/[pokemon].rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const POKEMON_API: &str = "https://pokeapi.co/api/v2/pokemon";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
id: u16,
weight: u16,
height: u16,
}
#[tuono_lib::handler]
async fn get_pokemon(req: Request, fetch: Client) -> Response {
// The param `pokemon` is defined in the route filename [pokemon].rs
let pokemon = req.params.get("pokemon").unwrap();
return match fetch.get(format!("{POKEMON_API}/{pokemon}")).send().await {
Ok(res) => {
++ if res.status() == StatusCode::NOT_FOUND {
++ return Response::Props(Props::new_with_status("{}", StatusCode::NOT_FOUND));
++ }
let data = res.json::<Pokemon>().await.unwrap();
Response::Props(Props::new(data))
}
-- Err(_err) => Response::Props(Props::new(
++ Err(_err) => Response::Props(Props::new_with_status(
++ "{}",
++ StatusCode::INTERNAL_SERVER_ERROR,
)),
};
}
```
```diff
// src/routes/index.rs
use serde::{Deserialize, Serialize};
-- use tuono_lib::reqwest::Client;
++ use tuono_lib::reqwest::{Client, StatusCode};
use tuono_lib::{Props, Request, Response};
const ALL_POKEMON: &str = "https://pokeapi.co/api/v2/pokemon?limit=151";
#[derive(Debug, Serialize, Deserialize)]
struct Pokemons {
results: Vec<Pokemon>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Pokemon {
name: String,
url: String,
}
#[tuono_lib::handler]
async fn get_all_pokemons(_req: Request, fetch: Client) -> Response {
return match fetch.get(ALL_POKEMON).send().await {
Ok(res) => {
let data = res.json::<Pokemons>().await.unwrap();
Response::Props(Props::new(data))
}
-- Err(_err) => Response::Props(Props::new(
++ Err(_err) => Response::Props(Props::new_with_status(
++ "{}", // Return empty JSON
++ StatusCode::INTERNAL_SERVER_ERROR,
)),
};
}
```
If you now try to load a not existing pokemon (`http://localhost:3000/pokemons/tuono-pokemon`) you will
correctly receive a 404 status code in the console.
## Seo and meta tags
The website now works and the http errors are meaningful but we should also take care to be meaningful
for the web crawlers. The best way to do it is to enrich the meta tags like the `<title>` and the
`<description>`.
To do so `tuono` exposes also the `<Head />` component useful exactly for handling this scenario. Let's update the `/` and the
`/pokemons/[pokemon]` routes with this.
```diff
// src/routes/index.tsx
import type { TuonoProps } from "tuono";
++ import { Head } from "tuono"
interface Pokemon {
name: string
}
interface IndexProps {
results: Pokemon[]
}
export default function IndexPage({
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>;
}
return (
<>
++ <Head>
++ <title>Tuono tutorial</title>
++ </Head>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: "wrap", display: "flex", gap: 10 }}>
{data.results.map((pokemon) => {
return pokemon.name;
})}
</ul>
</>
);
}
```
```diff
// src/routes/pokemons/[pokemon].tsx
-- import { TuonoProps } from "tuono";
++ import { TuonoProps, Head } from "tuono";
import PokemonView from "../../components/PokemonView";
export default function Pokemon({ data }: TuonoProps): JSX.Element {
-- return <PokemonView pokemon={data} />;
++ return (
++ <>
++ <Head>
++ <title>Pokemon: ${data?.name}</title>
++ </Head>
++ <PokemonView pokemon={data} />
++ </>
++ )
}
```
The `Head` component takes as children any valid HTML meta tag.
## Handle redirections
What if there is a pokemon among all of them that should be considered the GOAT? What
we are going to do right now is creating a new route `/pokemons/GOAT` that points to the best
pokemon of the first generation.
First let's create a new route by just creating an new file `/pokemons/GOAT.rs` and pasting the following code:
```rs
// src/routes/pokemons/GOAT.rs
use tuono_lib::{reqwest::Client, Request, Response};
#[tuono_lib::handler]
async fn redirect_to_goat(_: Request, _: Client) -> Response {
// Of course the GOAT is mewtwo - feel free to select your favourite 😉
Response::Redirect("/pokemons/mewtwo".to_string())
}
```
Now let's create the button in the home page to actually point to it!
```diff
// src/routes/index.tsx
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
++ <PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
```
Now at [http://localhost:3000/](http:/localhost:3000/) you will find a new link at the beginning of the list.
Click on it and see the application automatically redirecting you to your favourite pokemon's route!
## Building for production
The source now is ready to be released. Both server and client have been managed in a unoptimized way
to easy the development experience. To build the project to the production state just run:
```shell
$ tuono build
```
This command just created the final assets within the `out` directory. To run then the prodiction server
run:
```shell
$ cargo run --release
```
Check again [`http://localhost:3000/`](http://localhost:3000/) - This environment now has all the
optimizations ready to unleash the power of a rust server that seamessly renders a React application!🚀
> Note: The `out` directory is not standalone. You can't rely just on it to run the production server.
## Conclusion
Thats it! You just created a multi thread full stack application with rust and react.
The project is still under heavy development and many features are not ready yet but
I hope you got the taste of what is like working with rust and react in the same stack.
As I mentioned in the introduction I'd love to hear what you thought about the framework and the tutorial - feel free to reach me
at [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it) or in Twitter (X) DMs [@valerioageno](https://twitter.com/valerioageno).
+8
View File
@@ -0,0 +1,8 @@
import { execSync } from 'child_process'
import path from 'path'
const __dirname = import.meta.dirname
const rootDir = path.join(__dirname, '..')
execSync('cargo build --config opt-level=0', { cwd: rootDir, stdio: 'inherit' })
execSync('turbo build', { stdio: 'inherit' })
+20
View File
@@ -0,0 +1,20 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.tuono
out
target
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "tuono-app"
version = "0.0.1"
edition = "2021"
@@ -8,6 +8,6 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = { path = "../../crates/tuono_lib/"}
tuono_lib = { path = "../../../crates/tuono_lib" }
serde = { version = "1.0.202", features = ["derive"] }
+19
View File
@@ -0,0 +1,19 @@
{
"name": "fixture-base",
"type": "module",
"description": "Basic tuono application",
"version": "0.0.1",
"scripts": {
"test:e2e": "playwright test"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tuono": "workspace:*"
},
"devDependencies": {
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"typescript": "^5.6.3"
}
}
+26
View File
@@ -0,0 +1,26 @@
import * as path from 'path'
import { defineConfig } from '@playwright/test'
const __dirname = import.meta.dirname
const tuonoDir = path.join(__dirname, '../../../', 'target', 'debug', 'tuono')
const setupScript = path.join(__dirname, '../..', 'e2e-test-setup.js')
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
webServer: {
command: `node ${setupScript} && ${tuonoDir} dev`,
port: 3000,
timeout: 420 * 1000,
stdout: 'pipe',
reuseExistingServer: false,
},
use: {
baseURL: 'http://localhost:3000',
},
})
+17
View File
@@ -0,0 +1,17 @@
import type { ReactNode, JSX } from 'react'
import { TuonoScripts } from 'tuono'
interface RootLayoutProps {
children: ReactNode
}
export default function RootLayout({ children }: RootLayoutProps): JSX.Element {
return (
<html>
<body>
<main>{children}</main>
<TuonoScripts />
</body>
</html>
)
}
+14
View File
@@ -0,0 +1,14 @@
use serde::Serialize;
use tuono_lib::{Props, Request, Response};
#[derive(Serialize)]
struct MyResponse<'a> {
subtitle: &'a str,
}
#[tuono_lib::handler]
async fn get_server_side_props(_req: Request) -> Response {
Response::Props(Props::new(MyResponse {
subtitle: "Subtitle received from the server",
}))
}
+24
View File
@@ -0,0 +1,24 @@
import type { JSX } from 'react'
import type { TuonoProps } from 'tuono'
import { Link } from 'tuono'
interface IndexProps {
subtitle: string
}
export default function IndexPage({
data,
isLoading,
}: TuonoProps<IndexProps>): JSX.Element {
if (isLoading) {
return <h1>Loading...</h1>
}
return (
<>
<h1>TUONO</h1>
<h2>{data?.subtitle}</h2>
<Link href={'/second-route'}>Routing link</Link>)
</>
)
}
@@ -0,0 +1,5 @@
import type { JSX } from 'react'
export default function SecondRoute(): JSX.Element {
return <h1>Second route</h1>
}
+23
View File
@@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test'
test('it renders index page', async ({ page }) => {
await page.goto('/')
const header = await page.textContent('h1')
const subtitle = await page.textContent('h2')
expect(header).toContain('TUONO')
expect(subtitle).toContain('Subtitle received from the server')
})
test('it renders second route', async ({ page }) => {
await page.goto('/second-route')
const header = await page.textContent('h1')
expect(header).toContain('Second route')
})
test('it routes to second route on link click', async ({ page }) => {
await page.goto('/')
await page.click('text=Routing link')
await page.waitForURL('/second-route')
const header = await page.textContent('h1')
expect(header).toContain('Second route')
})
@@ -20,6 +20,5 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
"include": ["src", "tuono.config.ts"]
}
+5
View File
@@ -0,0 +1,5 @@
import type { TuonoConfig } from 'tuono/config'
const config: TuonoConfig = {}
export default config
+168
View File
@@ -0,0 +1,168 @@
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'
import eslintPluginReact from 'eslint-plugin-react'
// @ts-expect-error no types are available for this plugin
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
/** @type import('typescript-eslint').ConfigArray */
const tuonoEslintConfig = tseslint.config(
{
ignores: [
// #region shared
'**/dist',
'**/out',
'**/.tuono',
'**/vite.config.ts.timestamp**',
// #endregion shared
// #region package-specific
'packages/tuono-fs-router-vite-plugin/tests/generator/**',
'packages/tuono-lazy-fn-vite-plugin/tests/sources/**',
'packages/tuono/bin/**',
// #endregion package-specific
'examples/**',
],
},
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
},
eslint.configs.recommended,
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access */
eslintPluginImport.flatConfigs.recommended,
eslintPluginImport.flatConfigs.typescript,
/* 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,
// @ts-expect-error flat is optional but always defined on runtime
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat['jsx-runtime'],
{
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',
},
},
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: true,
},
react: {
version: 'detect',
},
},
rules: {
// #region @typescript-eslint
'@typescript-eslint/array-type': ['error', { default: 'generic' }],
'@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',
{
selector: 'typeParameter',
format: ['PascalCase'],
leadingUnderscore: 'forbid',
trailingUnderscore: 'forbid',
custom: {
regex: '^(T|T[A-Z][A-Za-z]+)$',
match: true,
},
},
],
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-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
'import/default': 'error',
'import/export': 'error',
'import/namespace': 'error',
'import/newline-after-import': 'error',
'import/no-cycle': 'error',
'import/no-duplicates': 'error',
'import/no-named-as-default-member': 'error',
'import/no-unused-modules': 'error',
'import/order': [
'error',
{
'newlines-between': 'always-and-inside-groups',
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
],
},
],
// #endregion import
// #region misc
/**
* @todo some of this might be removed.
* They are handled by other plugin and / or typescript
*/
'no-case-declarations': 'error',
'no-empty': 'error',
'no-prototype-builtins': 'error',
'no-redeclare': 'error',
'no-shadow': 'error',
'no-undef': 'off',
'sort-imports': 'off',
// #endregion misc
},
},
)
export default tuonoEslintConfig
+4 -15
View File
@@ -4,25 +4,14 @@ This folder includes all the official tuono starters.
To simply scaffold the base project run in your terminal:
```shell
$ tuono new [NAME]
```sh
tuono new [NAME]
```
You can install any example included in this folder by just using the `--template` flag:
```shell
$ tuono new [NAME] --template [TEMPLATE]
```sh
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
```
-16
View File
@@ -1,16 +0,0 @@
{
"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"
}
}
-14
View File
@@ -1,14 +0,0 @@
import type { ReactNode } from 'react'
import { MDXProvider } from '@mdx-js/react'
interface RootRouteProps {
children: ReactNode
}
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
return (
<main className="main">
<MDXProvider components={{}}>{children}</MDXProvider>
</main>
)
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
@@ -1,5 +1,5 @@
[package]
name = "tuono"
name = "tuono-app"
version = "0.0.1"
edition = "2021"
@@ -8,6 +8,6 @@ name = "tuono"
path = ".tuono/main.rs"
[dependencies]
tuono_lib = { path = "../../crates/tuono_lib/"}
tuono_lib = { path = "../../crates/tuono_lib/" }
serde = { version = "1.0.202", features = ["derive"] }
@@ -2,8 +2,8 @@
This is the starter tuono project. To download it run in your terminal:
```shell
$ tuono new [NAME]
```sh
tuono new my-first-tuono-app
```
+15
View File
@@ -0,0 +1,15 @@
{
"name": "tuono-app",
"description": "Basic tuono application",
"version": "0.0.1",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tuono": "link:../../packages/tuono"
},
"devDependencies": {
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"typescript": "^5.6.3"
}
}

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Some files were not shown because too many files have changed in this diff Show More