mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-29 13:52:46 -07:00
Compare commits
563 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de88199127 | |||
| 32d7d83d79 | |||
| 0a2ee1ab02 | |||
| 77c9e1f7c8 | |||
| 183f1c498d | |||
| 3e07d57956 | |||
| d5c0dd4365 | |||
| 4d187e506f | |||
| 71d3440d23 | |||
| 4856af992b | |||
| 6ccc91c4a8 | |||
| 468bc48a0c | |||
| 1ef6a9dc90 | |||
| 27c546d8e3 | |||
| fbaf411992 | |||
| 403d548a07 | |||
| be48dbda09 | |||
| 038d9e4a46 | |||
| a5dd7cb537 | |||
| 8ac2132a2e | |||
| bc49ec3193 | |||
| 022b548af1 | |||
| a1f5811eda | |||
| 5990cf3d27 | |||
| c0719c6edb | |||
| 9404946269 | |||
| eab8d67203 | |||
| a9a76f8b09 | |||
| f7ba80c5c4 | |||
| a4667c4c2c | |||
| 998a3191b4 | |||
| 89ad91a055 | |||
| d810047ddd | |||
| 19c73f7747 | |||
| 2f2d772882 | |||
| 32e300f730 | |||
| 2f31d14194 | |||
| 980aa0d07f | |||
| 07351587f3 | |||
| 3027b828a8 | |||
| eef2cd26e0 | |||
| 8190915e3a | |||
| dd157d6ef9 | |||
| 58f4ecb260 | |||
| ab441d1fa7 | |||
| 7538031261 | |||
| 18f66ff79b | |||
| b48f7244b7 | |||
| 22844a37fa | |||
| bc444929ed | |||
| 0813c14e15 | |||
| 5331cc0c8c | |||
| ba89a7e388 | |||
| 90a4d20436 | |||
| d938e82998 | |||
| fc15c01c90 | |||
| ffdf0f91ac | |||
| fa6b2f4c89 | |||
| 245b5c535b | |||
| 2bb0bdb2b0 | |||
| e368cc5e23 | |||
| eb4966e88e | |||
| 64c4c1174c | |||
| 315225184d | |||
| 39fcc33600 | |||
| 1adf380798 | |||
| ac20a83e7e | |||
| b17521063a | |||
| f449c4537e | |||
| 3c413c38bd | |||
| 750e4b6f22 | |||
| e6e455e966 | |||
| 54de22d8c4 | |||
| 40005c4e02 | |||
| 32a76a2560 | |||
| f3cf9b05ae | |||
| c946c43462 | |||
| 2ecfb0d5c0 | |||
| 6b02c40ad4 | |||
| 3334bddeb2 | |||
| 0335c8c1ad | |||
| 0fa83fb1a2 | |||
| b24466fb11 | |||
| c0b4fa8717 | |||
| 16b8125206 | |||
| 8ce1292960 | |||
| dbc205a09c | |||
| 0e5fcd4b57 | |||
| 42b7a22a29 | |||
| 13983a5362 | |||
| 557f412e27 | |||
| f8f21b69b6 | |||
| 7b656ee961 | |||
| c044ab45a7 | |||
| eda328f861 | |||
| f75fce87cf | |||
| 7c0999a5fe | |||
| a7b5e92c46 | |||
| 4d91bc0a72 | |||
| 3b3f601ff1 | |||
| abb12e7b3d | |||
| 66c3d91dfa | |||
| 53ec3ae280 | |||
| f293a1dee8 | |||
| a786f11d28 | |||
| 604581c64a | |||
| ae6ece7415 | |||
| b0a7557d83 | |||
| fde01421dc | |||
| 497532e6fe | |||
| 724ac83429 | |||
| c7ca9ca8b4 | |||
| 7934a7cbab | |||
| 35b1de5d22 | |||
| 2cfad3de12 | |||
| 78a742c4c8 | |||
| 57f0c9fa20 | |||
| 8dde08892c | |||
| da5fac0af2 | |||
| 12aa12c94d | |||
| 6dbdafa888 | |||
| 97082840f8 | |||
| d562aa87a3 | |||
| 2301083edc | |||
| 1c45b29ed1 | |||
| bfd0041c6a | |||
| 9f14689ca0 | |||
| b32b192f70 | |||
| 486a254a1e | |||
| 67de777fd1 | |||
| 8fcebae4a9 | |||
| 77c902a7a5 | |||
| da194524ee | |||
| 81e7af6f5e | |||
| fd2357cedc | |||
| 9db6f02701 | |||
| 9b6f8098b6 | |||
| a322ce2fad | |||
| 73b7a8f056 | |||
| 0e6546d4d0 | |||
| bf77ed1794 | |||
| e34c10bc34 | |||
| 310579cf76 | |||
| 1ba94238ed | |||
| 5f6bad637c | |||
| 3063763667 | |||
| caf1a58a0b | |||
| 059f3d6476 | |||
| 303a177344 | |||
| d26918fc2f | |||
| 31f0a167e9 | |||
| d71185c9cb | |||
| 798f8f1bf8 | |||
| 06612f07b0 | |||
| 067535c2f6 | |||
| d679982949 | |||
| de5ac53cc3 | |||
| ca4a7b3466 | |||
| 0a4d1e2995 | |||
| d6fc18e2f6 | |||
| 8e04abbe42 | |||
| 2905cfae3d | |||
| b632cf43ba | |||
| 06bc3700b7 | |||
| d4648afe01 | |||
| 0653349c08 | |||
| 12e0937174 | |||
| d37b655add | |||
| e3b1c0e013 | |||
| be77068c73 | |||
| 907c92bedc | |||
| 6794ab20d3 | |||
| e00dad9200 | |||
| 9aa6aedee0 | |||
| 0114867fe6 | |||
| 4f58d42582 | |||
| 40add60fd8 | |||
| 9a4bad95ed | |||
| 3e28aeea27 | |||
| 58e48147b8 | |||
| 68eda84053 | |||
| 253f35dbbb | |||
| 0f72506f95 | |||
| ebe1abe422 | |||
| f5a7480f69 | |||
| c0139fddc8 | |||
| 9e08a949bb | |||
| d86c97979b | |||
| 4e266522b6 | |||
| a58a1a4da9 | |||
| 22c1dd04c2 | |||
| 74e34dbbf9 | |||
| 4f38de3dea | |||
| 9d97f2d345 | |||
| 938cf3d8c6 | |||
| e1443274e3 | |||
| b9f8c0312f | |||
| 674a817131 | |||
| 2cbe53f7b7 | |||
| f3033ef0d2 | |||
| 0a0fb8c7f8 | |||
| f72a1a9e5c | |||
| fb29a26246 | |||
| 79d4aff758 | |||
| 7ea8414d6c | |||
| 1a3c9a7fbe | |||
| cecf2ba8cc | |||
| eaaf2513d7 | |||
| fa4d435320 | |||
| 2aff72942c | |||
| 11666fc422 | |||
| aef456cbf2 | |||
| 97b9d71caa | |||
| 5fa38b9506 | |||
| 98e50da575 | |||
| c8cf0771d4 | |||
| 1c6e78ad57 | |||
| 2b6f8c16ce | |||
| 2403ef3d64 | |||
| 003c0766cf | |||
| 9d7e59d793 | |||
| 43f03adedf | |||
| 211c6401e9 | |||
| 01b90b327e | |||
| ad0b09c3df | |||
| 362e1be58d | |||
| eb9039daa3 | |||
| ff7433f434 | |||
| a4e207688a | |||
| fdc87ed8f1 | |||
| 0ab9c297a5 | |||
| 04d6fcc1e1 | |||
| bf18ac0ab0 | |||
| bbe9605f6f | |||
| bd34c194cb | |||
| c0aa707315 | |||
| b6fa448202 | |||
| 2e141be0fc | |||
| 93b5e3e68f | |||
| 39dac5065f | |||
| a16a313f71 | |||
| 82560ddc20 | |||
| 83f3cae6f0 | |||
| 99df4a5575 | |||
| d33c211a01 | |||
| 21ee89ada4 | |||
| dcedb1c6c0 | |||
| d5cc65983e | |||
| 3cb150b090 | |||
| 305ae59086 | |||
| 8fe8930def | |||
| 8730bdbfb9 | |||
| 3ee47e9481 | |||
| 4e649acd60 | |||
| 4768d70a8a | |||
| c7bff7ed09 | |||
| 3035d2a645 | |||
| 0750037385 | |||
| 8c4d61ee65 | |||
| 1a3ccb112e | |||
| 118172d27f | |||
| 69f1d85360 | |||
| f0d042c85b | |||
| 550830c232 | |||
| 6be12b4e58 | |||
| 33aa37ed7d | |||
| 43910fd75d | |||
| 5efcc86a63 | |||
| 17b61a5a41 | |||
| 5dae5c31b7 | |||
| 96deea7f31 | |||
| 7632e6a464 | |||
| 23e2a2e3ff | |||
| b19512dffc | |||
| 11c6fc820b | |||
| 1a8231851d | |||
| 1dfb83b1aa | |||
| 93cb1ae8ba | |||
| 631b168e05 | |||
| af8a43c93a | |||
| 875d12f514 | |||
| a9a2ef67ea | |||
| 5caf3ed6e7 | |||
| 580ec704f2 | |||
| a9918ae555 | |||
| 4b3f4bfab8 | |||
| fed3b1581b | |||
| 9b2f0aab32 | |||
| 818a6bd2ff | |||
| 94a43347a1 | |||
| 16d793c588 | |||
| 0322e5bd8d | |||
| cfe76a9ab3 | |||
| ff6014317b | |||
| 64220f2e27 | |||
| e75414abfc | |||
| d6fae42dc3 | |||
| 0db1245a6b | |||
| 91c0a9ec31 | |||
| 7670c37685 | |||
| ae41c5629c | |||
| 3e7fa05fce | |||
| 6833640c47 | |||
| 18541c45de | |||
| 45bb0dd0d2 | |||
| db0aad2c37 | |||
| d55ca12125 | |||
| ad2cada94b | |||
| f9545f513b | |||
| c75d83dc7c | |||
| 91236e5533 | |||
| 1356be248e | |||
| 7e8be92cd5 | |||
| c3fda4a3a5 | |||
| dfc66cfd50 | |||
| 0ef1252e1f | |||
| cf56b3a53e | |||
| 592f402ffd | |||
| 1dc77fc473 | |||
| 09cd772d74 | |||
| 1c69e10e6a | |||
| 1af18e4c91 | |||
| a3709609c5 | |||
| 70a79faee0 | |||
| 07cb7dae9d | |||
| 9a1fb9c4a1 | |||
| a3f7ed7f61 | |||
| 5302fd2779 | |||
| ccd4e02b6a | |||
| f6593097b6 | |||
| 230cd22bff | |||
| f4367f1755 | |||
| 7b7447f9a0 | |||
| d12d8404ec | |||
| d1b12173d5 | |||
| f3bf13da59 | |||
| f236b75f55 | |||
| 2434231f9b | |||
| b8b39e69a1 | |||
| f8158dfd8e | |||
| f1ab7df5ce | |||
| 986de49052 | |||
| d09ca5548a | |||
| 77e22c40b6 | |||
| de74647c43 | |||
| 498601c21a | |||
| e99f06ddf5 | |||
| f06af59168 | |||
| d50e3a7fc0 | |||
| 3e4e7ff1ab | |||
| 21ff3c8655 | |||
| 166e61bab4 | |||
| 93cd8bd8da | |||
| f5c1641af2 | |||
| 62ffed0624 | |||
| 8d0a4a7417 | |||
| b1b274ea12 | |||
| fc38865965 | |||
| 911adaaecb | |||
| 7626ed8d1d | |||
| 6135fd7e1b | |||
| 14bea36356 | |||
| 4abc163a59 | |||
| 5bb9cf1e7c | |||
| a0cdf408a1 | |||
| 8467506915 | |||
| 704255774d | |||
| 10bb7b1cca | |||
| 7f82af3d3b | |||
| d7c729c035 | |||
| 51fa904f9b | |||
| 6191e55745 | |||
| c6e93c266a | |||
| bcf7e5b5ab | |||
| 466f122cc4 | |||
| 539484ea25 | |||
| db1cee25bf | |||
| 96502b18d0 | |||
| c491ece8c5 | |||
| c76464bf8a | |||
| 08b75b1a79 | |||
| 90da869277 | |||
| 80c4affaa8 | |||
| aa087193fe | |||
| af7e51f69b | |||
| 1b62d8ddc2 | |||
| 0839579733 | |||
| 7f9bf08f76 | |||
| 79ec1c6922 | |||
| c68246df83 | |||
| abc0ad7120 | |||
| 4356ca1cff | |||
| d6215d009c | |||
| 0b501e95a5 | |||
| f157ff37e0 | |||
| 11d425faa9 | |||
| 5fc68a1926 | |||
| ec4577c187 | |||
| 8052de8ad5 | |||
| 2fda959cd6 | |||
| 05fa8a5ae2 | |||
| a341b41e9a | |||
| a2e0a4d78a | |||
| 4fb1d6077c | |||
| 53ab927544 | |||
| 897cc2daf2 | |||
| 73ff31daf5 | |||
| c387a9e17e | |||
| 1f92931076 | |||
| d3105a1532 | |||
| 7f50f31837 | |||
| 4c3e90ea4b | |||
| 8d80fc4aa9 | |||
| 78a04ffcf1 | |||
| bcb2c91823 | |||
| 9058f209dd | |||
| 1f172d583e | |||
| 439b4757df | |||
| 9f8809bd51 | |||
| 4e326650cc | |||
| e27be27692 | |||
| 88eb2a1897 | |||
| 8c84ef73c8 | |||
| 859e49f16c | |||
| 14ba8b0a23 | |||
| d7c70f75f3 | |||
| 2760e3ea08 | |||
| f2f041fe55 | |||
| 9ec9700ed4 | |||
| c6b047fc02 | |||
| 6727264e39 | |||
| d1a5fe234d | |||
| e84c7cd276 | |||
| 087813fab9 | |||
| 3a4e7398c6 | |||
| 1630fd7e71 | |||
| 4aa99beaff | |||
| eaa0ba28d8 | |||
| 3f3e1120b2 | |||
| 89d7a2c740 | |||
| 56c31f0204 | |||
| 338cfb5be2 | |||
| 43f6fe664b | |||
| 209c9fb7f7 | |||
| a562f67e8d | |||
| ef0fe5a425 | |||
| b50902d23e | |||
| 55490579ec | |||
| 957d08701f | |||
| 61e1aed35d | |||
| 2034f0ef5f | |||
| fcb92c7f0b | |||
| 28ea1ca1df | |||
| ebc1a458a9 | |||
| df92ef5296 | |||
| 253e0a8378 | |||
| a9fbe4eddd | |||
| cfccc638c6 | |||
| 9f1460cc26 | |||
| f0a4541420 | |||
| d0ce482680 | |||
| 40e2734a4e | |||
| 8552796343 | |||
| 6d72ffb876 | |||
| 65faaf0183 | |||
| 7f7c2e8d6f | |||
| 3be491baa7 | |||
| 48b974ae58 | |||
| c394898fcd | |||
| f22249f3e9 | |||
| 120cec52e9 | |||
| 535cc6b3d9 | |||
| 18a060e2bf | |||
| 7b8165cee6 | |||
| 6331eb64e6 | |||
| 3c38b551f0 | |||
| 8e761347f2 | |||
| e2d44e0e6a | |||
| 607da92c71 | |||
| ae0427604e | |||
| 2aa981c581 | |||
| 5d8ec82502 | |||
| fc9bb52c41 | |||
| 3224e4138f | |||
| 7ea28c4fd3 | |||
| acb5567f6a | |||
| cc877c7c11 | |||
| 87bdddf7cf | |||
| d939b64d64 | |||
| 3f64f71947 | |||
| aaae7db24d | |||
| 48c3934b6e | |||
| 258b4af0b1 | |||
| edfbd03f13 | |||
| 9cb0f62da7 | |||
| 9591960e31 | |||
| 132d8ebe8f | |||
| 4020073aca | |||
| 5025692220 | |||
| d04a4c4103 | |||
| 7e6dea68f4 | |||
| a046833414 | |||
| cfa59ca8bf | |||
| d470bd58f2 | |||
| ea82d2b1df | |||
| 93b44088a9 | |||
| 625c091fdc | |||
| e0c0447fb6 | |||
| b67a8e8537 | |||
| c9f1b02954 | |||
| 6b3bfca1f6 | |||
| 15e568594d | |||
| 089f41ee89 | |||
| 636c68e532 | |||
| 6d8a04dfb2 | |||
| 01d5aad8aa | |||
| 002733cd99 | |||
| d7a086dfb1 | |||
| d60c7dcd9b | |||
| 58baf8aa82 | |||
| 0a8504d9f2 | |||
| 934a83c0e2 | |||
| ea65ad042f | |||
| 3544cd2943 | |||
| e593707ce7 | |||
| 6dd0fc7410 | |||
| faf7bf29e2 | |||
| c5b5f6aefe | |||
| 8e48eec01d | |||
| e3c5cc882f | |||
| dfb1bebc6b | |||
| a40367f813 | |||
| 1a1b62698c | |||
| b36dc0b897 | |||
| a9eacf21f4 | |||
| 23d84b25be | |||
| 6c7cc62371 | |||
| 588326cc57 | |||
| ce654bf7a2 | |||
| a3bcb76105 | |||
| 4045ebc24e | |||
| 2be3710462 | |||
| fe3185c6dc | |||
| f10b813e1e | |||
| c67d4199a0 | |||
| d0a6c59c62 | |||
| 60f622542f | |||
| e7fcd64b3a | |||
| 3772e7307e | |||
| e853db164d | |||
| 509283adab | |||
| db56b7f2dc | |||
| acd663d79f | |||
| 3636268a71 | |||
| 6402f42d01 | |||
| 71ac19cbb9 | |||
| d99389a398 | |||
| 024c386af4 | |||
| d982c9c24b | |||
| 480973318d | |||
| a497937106 | |||
| 0d33150f30 | |||
| 94667b1454 |
@@ -0,0 +1,10 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
insert_final_newline = true
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
max_line_length = 80
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_size = 4
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1 +1,10 @@
|
|||||||
examples/** linguist-documentation
|
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
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# @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:
|
||||||
|
[
|
||||||
|
'devtools/**',
|
||||||
|
'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:
|
||||||
|
[
|
||||||
|
'devtools/**',
|
||||||
|
'.npmrc',
|
||||||
|
'.nvmrc',
|
||||||
|
'.prettierrc',
|
||||||
|
'.prettierignore',
|
||||||
|
'renovate.json',
|
||||||
|
'turbo.json',
|
||||||
|
]
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
## Context & Description
|
<!--
|
||||||
|
👋 Thank you for your Pull Request 🙏
|
||||||
|
-->
|
||||||
|
|
||||||
|
### 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.
|
|
||||||
|
|
||||||
Explain the context and why you're making that change. What is the problem
|
Explain the context and why you're making that change.
|
||||||
you're trying to solve? If a new feature is being added, describe the intended
|
What is the problem you're trying to solve?
|
||||||
use case that feature fulfills.
|
If a new feature is being added,
|
||||||
|
describe the intended use case that feature fulfills.
|
||||||
|
|
||||||
Bug fixes and new features should include tests.
|
|
||||||
|
|
||||||
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
|
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
name: Documentation Website CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '.github/**'
|
|
||||||
- 'apps/documentation/**'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '.github/**'
|
|
||||||
- 'apps/documentation/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
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
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./apps/documentation
|
|
||||||
|
|
||||||
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: Setup rust
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
|
|
||||||
- name: Install tuono
|
|
||||||
run: cargo install tuono@0.8.3
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm i -g pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --ignore-workspace
|
|
||||||
|
|
||||||
- name: Build project
|
|
||||||
run: tuono build --static
|
|
||||||
|
|
||||||
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
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./apps/documentation
|
|
||||||
|
|
||||||
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 --ignore-workspace
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: pnpm format:check
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: pnpm lint
|
|
||||||
|
|
||||||
- name: Types
|
|
||||||
run: pnpm types
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
name: Deploy documentation website on AWS S3
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '.github/**'
|
|
||||||
- 'apps/documentation/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy-documentation:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./apps/documentation
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
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: Setup rust
|
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
||||||
|
|
||||||
- name: Install tuono
|
|
||||||
run: cargo install tuono@0.8.3
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
run: npm i -g pnpm
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install --ignore-workspace
|
|
||||||
|
|
||||||
- name: Build project
|
|
||||||
run: tuono build --static
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: reggionick/s3-deploy@v4
|
|
||||||
with:
|
|
||||||
folder: apps/documentation/out/static
|
|
||||||
bucket: tuono-documentation
|
|
||||||
bucket-region: eu-west-3
|
|
||||||
dist-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}
|
|
||||||
invalidation: /
|
|
||||||
delete-removed: true
|
|
||||||
no-cache: true
|
|
||||||
private: true
|
|
||||||
files-to-include: '{.*/**,**}'
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
steps:
|
||||||
|
- name: Exit with error if some jobs are not successful
|
||||||
|
run: exit 1
|
||||||
|
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Examples 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:
|
||||||
|
example_list:
|
||||||
|
name: Get examples names
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 1
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
names: ${{ steps.find_folder.outputs.names }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- id: find_folder
|
||||||
|
working-directory: examples
|
||||||
|
# produces the list of folder inside examples without leading `./` and trailing `/`:
|
||||||
|
# ```text
|
||||||
|
# tuono-tutorial
|
||||||
|
# with-tailwind
|
||||||
|
# tuono-app
|
||||||
|
# with-mdx
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# after, using `jq` the output is converted into a JSON array:
|
||||||
|
# ```json
|
||||||
|
# ["tuono-tutorial","with-tailwind","tuono-app","with-mdx"]
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# and added to `$GITHUB_OUTPUT` env variable:
|
||||||
|
run: |
|
||||||
|
NAMES=$(find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)
|
||||||
|
echo "names=$(echo "$NAMES" | jq -R -s -c 'split("\n")[:-1]')" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
check_example:
|
||||||
|
needs:
|
||||||
|
- example_list
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
example_name: ${{ fromJson(needs.example_list.outputs.names) }}
|
||||||
|
os:
|
||||||
|
- 'ubuntu-latest'
|
||||||
|
- 'macos-latest'
|
||||||
|
# to be added
|
||||||
|
# - 'windows-latest'
|
||||||
|
|
||||||
|
# Only on this repo some examples build including plugins fails
|
||||||
|
# However, if the project is downloaded via `tuono new`` it will work
|
||||||
|
# @see https://github.com/tuono-labs/tuono/pull/605#pullrequestreview-2656262230
|
||||||
|
# needs further investigations
|
||||||
|
exclude:
|
||||||
|
- example_name: with-tailwind
|
||||||
|
|
||||||
|
name: 'Check `${{ matrix.example_name }}` build on ${{ matrix.os }}'
|
||||||
|
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: Build Rust
|
||||||
|
run: cargo build
|
||||||
|
|
||||||
|
- name: Build typescript
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Check "build"
|
||||||
|
working-directory: ./examples/${{ matrix.example_name }}
|
||||||
|
run: ../../target/debug/tuono build
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: pnpm run format
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm run typecheck
|
||||||
|
|
||||||
|
ci_ok:
|
||||||
|
name: Examples CI OK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 1
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- example_list
|
||||||
|
- check_example
|
||||||
|
steps:
|
||||||
|
- name: Exit with error if some jobs are not successful
|
||||||
|
run: exit 1
|
||||||
|
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,30 +1,23 @@
|
|||||||
name: Publish on npm and crates
|
name: Publish on npm and crates
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-on-npm-dry-run:
|
publish_npm_dry_run:
|
||||||
|
name: NPM publish (dry run)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install NodeJS Dependencies
|
||||||
uses: actions/setup-node@v4
|
uses: ./.github/actions/install-node-dependencies
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
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
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
@@ -33,46 +26,43 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
publish-on-crates-dry-run:
|
publish_crates_dry_run:
|
||||||
|
name: Crates publish (dry run)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout code
|
||||||
- uses: actions-rs/toolchain@v1
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
toolchain: stable
|
- name: Setup rust toolchain
|
||||||
override: true
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
- uses: katyo/publish-crates@v2
|
with:
|
||||||
with:
|
toolchain: 'stable'
|
||||||
|
|
||||||
|
- uses: katyo/publish-crates@v2
|
||||||
|
with:
|
||||||
path: './crates/tuono'
|
path: './crates/tuono'
|
||||||
dry-run: true
|
dry-run: true
|
||||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
check-repo: true
|
check-repo: true
|
||||||
|
|
||||||
publish-on-npm:
|
publish_npm:
|
||||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
name: NPM publish
|
||||||
|
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Node.js
|
- name: Install NodeJS Dependencies
|
||||||
uses: actions/setup-node@v4
|
uses: ./.github/actions/install-node-dependencies
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
|
||||||
registry-url: 'https://registry.npmjs.org'
|
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
|
- name: Clone root README.md on tuono package
|
||||||
run: cp README.md packages/tuono/README.md
|
run: cp README.md packages/tuono/README.md
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
@@ -81,17 +71,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
publish-on-crates:
|
publish_crates:
|
||||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
name: Crates publish
|
||||||
|
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: Checkout code
|
||||||
- uses: actions-rs/toolchain@v1
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
toolchain: stable
|
- name: Setup rust toolchain
|
||||||
override: true
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
- uses: katyo/publish-crates@v2
|
with:
|
||||||
with:
|
toolchain: 'stable'
|
||||||
|
|
||||||
|
- uses: katyo/publish-crates@v2
|
||||||
|
with:
|
||||||
path: './crates/tuono'
|
path: './crates/tuono'
|
||||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
check-repo: true
|
check-repo: true
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
ci_ok:
|
||||||
|
name: Repo root CI OK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 1
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- format
|
||||||
|
steps:
|
||||||
|
- name: Exit with error if some jobs are not successful
|
||||||
|
run: exit 1
|
||||||
|
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
steps:
|
||||||
|
- name: Exit with error if some jobs are not successful
|
||||||
|
run: exit 1
|
||||||
|
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
|
||||||
@@ -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
|
|
||||||
@@ -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: ''
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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 run format
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Typecheck
|
||||||
|
run: pnpm run typecheck
|
||||||
|
|
||||||
|
ci_ok:
|
||||||
|
name: Typescript CI OK
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 1
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- build_and_test
|
||||||
|
- lint_and_fmt
|
||||||
|
steps:
|
||||||
|
- name: Exit with error if some jobs are not successful
|
||||||
|
run: exit 1
|
||||||
|
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')) }}
|
||||||
@@ -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
@@ -1,12 +1,14 @@
|
|||||||
|
## Node related ignores
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.turbo/
|
.turbo/
|
||||||
|
|
||||||
*/**/routeTree.gen.ts
|
*/**/routeTree.gen.ts
|
||||||
|
|
||||||
pnpm-lock.yaml
|
examples/*/pnpm-lock.yaml
|
||||||
|
|
||||||
examples/playground/
|
vite.config.ts.timestamp*
|
||||||
|
|
||||||
## Rust related ignores
|
## Rust related ignores
|
||||||
|
|
||||||
@@ -24,3 +26,7 @@ examples/playground/
|
|||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
|
# Files, generated by the code editors
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
dist
|
||||||
|
.tuono
|
||||||
|
out
|
||||||
|
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
|
packages/tuono-lazy-fn-vite-plugin/tests/sources/*
|
||||||
|
|
||||||
|
e2e/fixtures/*/test-results
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
"semi": false,
|
"semi": false,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all"
|
"trailingComma": "all"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* @Valerioageno
|
||||||
|
|
||||||
+15
-15
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## 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
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
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
|
Examples of behavior that contributes to a positive environment for our
|
||||||
community include:
|
community include:
|
||||||
|
|
||||||
* Demonstrating empathy and kindness toward other people
|
- Demonstrating empathy and kindness toward other people
|
||||||
* Being respectful of differing opinions, viewpoints, and experiences
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
* Giving and gracefully accepting constructive feedback
|
- Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
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
|
overall community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
advances of any kind
|
Advances of any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
- Publishing others' private information, such as a physical or email
|
||||||
address, without their explicit permission
|
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
|
professional setting
|
||||||
|
|
||||||
## Enforcement Responsibilities
|
## Enforcement Responsibilities
|
||||||
@@ -45,13 +45,13 @@ or harmful.
|
|||||||
|
|
||||||
Community leaders have the right and responsibility to remove, edit, or reject
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
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.
|
decisions when appropriate.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
This Code of Conduct applies within all community spaces, and also applies when
|
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,
|
Examples of representing our community include using an official e-mail address,
|
||||||
posting via an official social media account, or acting as an appointed
|
posting via an official social media account, or acting as an appointed
|
||||||
representative at an online or offline event.
|
representative at an online or offline event.
|
||||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
|||||||
### 4. Permanent Ban
|
### 4. Permanent Ban
|
||||||
|
|
||||||
**Community Impact**: Demonstrating a pattern of violation of community
|
**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.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
|||||||
+15
-10
@@ -1,21 +1,26 @@
|
|||||||

|

|
||||||

|

|
||||||
[](https://opensource.org/licenses/MIT)
|
[](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.
|
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
|
## Bugs
|
||||||
|
|
||||||
**Did you find a bug?**
|
**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?**
|
**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.
|
[Visit the documentation to learn how to open a pull request](https://tuono.dev/documentation/contributing/pull-requests)
|
||||||
- The pull requests must pass all the CI pipelines
|
|
||||||
|
|||||||
+9
-7
@@ -1,13 +1,15 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/tuono",
|
"crates/tuono",
|
||||||
"crates/tuono_lib",
|
"crates/tuono_internal",
|
||||||
"crates/tuono_lib_macros",
|
"crates/tuono_lib",
|
||||||
|
"crates/tuono_lib_macros"
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"apps/documentation",
|
"examples/with-mdx",
|
||||||
"examples/mdx",
|
"examples/tuono-app",
|
||||||
"examples/tuono",
|
"examples/tuono-tutorial",
|
||||||
"examples/tutorial"
|
"examples/with-tailwind",
|
||||||
|
"e2e/fixtures/base",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,98 +1,62 @@
|
|||||||
[Documentation](https://tuono.dev) | [Tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) |
|
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
|
||||||
[✨Contributing](https://tuono.dev/documentation/contributing)
|
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
|
||||||
|
|
||||||
# Tuono
|
# Tuono
|
||||||
|
|
||||||
<img align="right" src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="160px">
|
<img align="right" src="https://raw.githubusercontent.com/tuono-labs/tuono/main/assets/logo.png" width="160px">
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
|
||||||
**Tuono** is the react/rust fullstack framework.
|
a strong focus on usability and performance.**
|
||||||
|
|
||||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||||
Why Tuono? Just a badass name.
|
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.
|
## Documentation
|
||||||
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
|
|
||||||
React is still React - it is just superpowered.
|
|
||||||
|
|
||||||
**Rust is an hard language then writing server side code is hard as well right? (no again)**
|
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.
|
## Installation
|
||||||
Each server side route is managed with a separate file alongside the React route. The routing is handled
|
|
||||||
by Tuono based on the files defined within the `./src/routes` directory.
|
|
||||||
|
|
||||||
## Features
|
Available on MacOS, Linux and Windows.
|
||||||
|
|
||||||
- 🟦 Typescript
|
```sh
|
||||||
- 🌐 Routing
|
cargo install tuono
|
||||||
- 🔥 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
|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
As already mentioned above I strongly suggest you to take a look at the
|
Create a new project with:
|
||||||
[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
|
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tuono new [PROJECT_NAME]
|
||||||
```
|
```
|
||||||
├── package.json
|
|
||||||
├── public
|
> You can optionally pass the --template (or -t) flag to directly start from a [template](https://github.com/tuono-labs/tuono/tree/main/examples)
|
||||||
├── src
|
|
||||||
│ ├── routes
|
Now just install the dependencies with you favourite JS package manager (i.e. `npm install`) and start the development environment
|
||||||
│ └── styles
|
|
||||||
├── Cargo.toml
|
```sh
|
||||||
├── README.md
|
tuono dev
|
||||||
└── tsconfig.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Any help or suggestion will be appreciated and encouraged.
|
Any help or suggestion will be appreciated and encouraged.
|
||||||
|
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# Stadalone apps folder
|
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Tuono starter
|
|
||||||
|
|
||||||
This is the starter tuono project. To download it run in your terminal:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ tuono new [NAME]
|
|
||||||
```
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tuono",
|
|
||||||
"description": "The react/rust fullstack framework",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"scripts": {
|
|
||||||
"lint": "eslint --ext .ts,.tsx ./src -c .eslintrc",
|
|
||||||
"format": "prettier -u --write --ignore-unknown './src/**/*'",
|
|
||||||
"format:check": "prettier --check --ignore-unknown './src/**/*'",
|
|
||||||
"types": "tsc --noEmit"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@mantine/code-highlight": "^7.11.2",
|
|
||||||
"@mantine/core": "^7.11.2",
|
|
||||||
"@mantine/hooks": "^7.11.2",
|
|
||||||
"@mdx-js/react": "^3.0.1",
|
|
||||||
"@tabler/icons-react": "^3.11.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"tuono": "0.9.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/mdx": "^2.0.13",
|
|
||||||
"@types/react": "^18.3.3",
|
|
||||||
"@types/react-dom": "^18.3.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.7.1",
|
|
||||||
"@typescript-eslint/parser": "^7.7.1",
|
|
||||||
"eslint": "^8.56.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
|
||||||
"eslint-plugin-react": "^7.33.2",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"postcss": "^8.4.39",
|
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
|
||||||
"postcss-simple-vars": "^7.0.1",
|
|
||||||
"prettier": "^3.2.4",
|
|
||||||
"typescript": "^5.5.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-preset-mantine': {},
|
|
||||||
'postcss-simple-vars': {
|
|
||||||
variables: {
|
|
||||||
'mantine-breakpoint-xs': '36em',
|
|
||||||
'mantine-breakpoint-sm': '48em',
|
|
||||||
'mantine-breakpoint-md': '62em',
|
|
||||||
'mantine-breakpoint-lg': '75em',
|
|
||||||
'mantine-breakpoint-xl': '88em',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,54 +0,0 @@
|
|||||||
import { Breadcrumbs, Button } from '@mantine/core'
|
|
||||||
import { Link } from 'tuono'
|
|
||||||
|
|
||||||
import { IconChevronRight, IconBolt } from '@tabler/icons-react'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
|
|
||||||
export default function TuonoBreadcrumbs({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Breadcrumbs
|
|
||||||
separator={<IconChevronRight size="1.1rem" stroke={1.5} />}
|
|
||||||
mb="md"
|
|
||||||
mt="md"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
href="/documentation"
|
|
||||||
component={Link}
|
|
||||||
variant="subtle"
|
|
||||||
radius="xl"
|
|
||||||
p={5}
|
|
||||||
>
|
|
||||||
<IconBolt />
|
|
||||||
</Button>
|
|
||||||
{children}
|
|
||||||
</Breadcrumbs>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BreadcrumbElementProps {
|
|
||||||
href?: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BreadcrumbElement({
|
|
||||||
href,
|
|
||||||
label,
|
|
||||||
}: BreadcrumbElementProps): JSX.Element {
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<Button component={Link} href={href} variant="subtle" radius="xl" px={10}>
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button component="span" variant="light" radius="xl">
|
|
||||||
{label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Breadcrumbs, { BreadcrumbElement } from './breadcrumbs'
|
|
||||||
|
|
||||||
export default Breadcrumbs
|
|
||||||
|
|
||||||
export { BreadcrumbElement as Element }
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Button } from '@mantine/core'
|
|
||||||
import { IconEdit } from '@tabler/icons-react'
|
|
||||||
import { useRouter } from 'tuono'
|
|
||||||
|
|
||||||
const GITHUB_URL =
|
|
||||||
'https://github.com/Valerioageno/tuono/tree/main/apps/documentation/src/routes'
|
|
||||||
|
|
||||||
export default function EditPage(): JSX.Element {
|
|
||||||
const { pathname } = useRouter()
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
p={0}
|
|
||||||
mt={60}
|
|
||||||
component="a"
|
|
||||||
variant="transparent"
|
|
||||||
leftSection={<IconEdit />}
|
|
||||||
target="_blank"
|
|
||||||
href={GITHUB_URL.concat(pathname).concat('.mdx')}
|
|
||||||
>
|
|
||||||
Edit page
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import EditPage from './edit-page'
|
|
||||||
|
|
||||||
export default EditPage
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Center,
|
|
||||||
Container,
|
|
||||||
CopyButton,
|
|
||||||
Group,
|
|
||||||
rem,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { IconCopy, IconCheck } from '@tabler/icons-react'
|
|
||||||
import { Link } from 'tuono'
|
|
||||||
|
|
||||||
export default function Hero(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Container size={1100} mt={200}>
|
|
||||||
<Center>
|
|
||||||
<Title order={1}>The react/rust fullstack framework</Title>
|
|
||||||
</Center>
|
|
||||||
<Center mt={50}>
|
|
||||||
<Text fz="18px">
|
|
||||||
The technologies we love seamessly working together to unleash the
|
|
||||||
<strong> highest web performance</strong> ever met on react
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
<Center mt={50}>
|
|
||||||
<Group>
|
|
||||||
<Button component={Link} href="/documentation" size="lg">
|
|
||||||
Get Started
|
|
||||||
</Button>
|
|
||||||
<CopyButton value="cargo install tuono">
|
|
||||||
{({ copied, copy }) => (
|
|
||||||
<Button
|
|
||||||
onClick={copy}
|
|
||||||
size="lg"
|
|
||||||
style={{ border: 'solid 1px var(--mantine-color-violet-1)' }}
|
|
||||||
color="gray"
|
|
||||||
leftSection="cargo install tuono"
|
|
||||||
rightSection={
|
|
||||||
copied ? (
|
|
||||||
<IconCheck style={{ width: rem(20) }} />
|
|
||||||
) : (
|
|
||||||
<IconCopy style={{ width: rem(20) }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CopyButton>
|
|
||||||
</Group>
|
|
||||||
</Center>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Hero from './hero'
|
|
||||||
|
|
||||||
export default Hero
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxProvider from './mdx-provider'
|
|
||||||
|
|
||||||
export default MdxProvider
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Text, type TextProps } from '@mantine/core'
|
|
||||||
|
|
||||||
export default function MdxBold(props: TextProps): JSX.Element {
|
|
||||||
return <Text fw={700} {...props} />
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxCode from './mdx-code'
|
|
||||||
|
|
||||||
export default MdxCode
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Code } from '@mantine/core'
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
export default function MdxCode(
|
|
||||||
props: HTMLAttributes<HTMLPreElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
return <Code {...props} />
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Title } from '@mantine/core'
|
|
||||||
import { useRef, type HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
export default function MdxH2(
|
|
||||||
props: HTMLAttributes<HTMLHeadingElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Title
|
|
||||||
{...props}
|
|
||||||
mt={20}
|
|
||||||
order={2}
|
|
||||||
id={String(props.children ?? '')
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll(' ', '-')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxLink from './mdx-link'
|
|
||||||
|
|
||||||
export default MdxLink
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type { AnchorHTMLAttributes } from 'react'
|
|
||||||
import { Button } from '@mantine/core'
|
|
||||||
import { Link } from 'tuono'
|
|
||||||
import { IconExternalLink } from '@tabler/icons-react'
|
|
||||||
|
|
||||||
export default function MdxLink(
|
|
||||||
props: AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
if (props.href?.startsWith('http') || props.href?.startsWith('mailto')) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
{...props}
|
|
||||||
target="_blank"
|
|
||||||
rightSection={
|
|
||||||
<IconExternalLink size="16px" style={{ marginLeft: -5 }} />
|
|
||||||
}
|
|
||||||
variant="transparent"
|
|
||||||
style={{ height: '20px' }}
|
|
||||||
mt={-2}
|
|
||||||
p={0}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
component={Link}
|
|
||||||
{...props}
|
|
||||||
target="_blank"
|
|
||||||
variant="transparent"
|
|
||||||
style={{ height: '20px' }}
|
|
||||||
mt={-2}
|
|
||||||
p={0}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxPre from './mdx-pre'
|
|
||||||
|
|
||||||
export default MdxPre
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.pre {
|
|
||||||
code {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { CodeHighlight } from '@mantine/code-highlight'
|
|
||||||
import styles from './mdx-pre.module.css'
|
|
||||||
|
|
||||||
interface PreProps {
|
|
||||||
children: any
|
|
||||||
}
|
|
||||||
export default function MdxPre({ children }: PreProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<CodeHighlight
|
|
||||||
className={styles.pre}
|
|
||||||
style={{ borderRadius: 8 }}
|
|
||||||
code={children.props.children || ''}
|
|
||||||
language={children.props.className?.replace('language-', '')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { MDXProvider } from '@mdx-js/react'
|
|
||||||
|
|
||||||
import MdxLink from './mdx-link'
|
|
||||||
import type { ReactNode } from 'react'
|
|
||||||
import MdxPre from './mdx-pre'
|
|
||||||
import MdxQuote from './mdx-quote'
|
|
||||||
import MdxCode from './mdx-code'
|
|
||||||
import MdxTitle from './mdx-title'
|
|
||||||
import MdxBold from './mdx-bold/mdx-bold'
|
|
||||||
import MdxH2 from './mdx-h2/mdx-h2'
|
|
||||||
|
|
||||||
interface MdxProviderProps {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MdxProvider({
|
|
||||||
children,
|
|
||||||
}: MdxProviderProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<MDXProvider
|
|
||||||
components={{
|
|
||||||
a: MdxLink,
|
|
||||||
// @ts-expect-error: useless finding the correct props types
|
|
||||||
pre: MdxPre,
|
|
||||||
blockquote: MdxQuote,
|
|
||||||
code: MdxCode,
|
|
||||||
h1: MdxTitle,
|
|
||||||
h2: MdxH2,
|
|
||||||
strong: MdxBold,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</MDXProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxQuote from './mdx-quote'
|
|
||||||
|
|
||||||
export default MdxQuote
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Blockquote, Space } from '@mantine/core'
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
export default function MdxQuote(
|
|
||||||
props: HTMLAttributes<HTMLQuoteElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Blockquote
|
|
||||||
color="violet"
|
|
||||||
py={1}
|
|
||||||
px={20}
|
|
||||||
mt={30}
|
|
||||||
iconSize={30}
|
|
||||||
{...props}
|
|
||||||
style={{ borderRadius: 8 }}
|
|
||||||
/>
|
|
||||||
<Space h="md" />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import MdxTitle from './mdx-title'
|
|
||||||
|
|
||||||
export default MdxTitle
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Title } from '@mantine/core'
|
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
|
|
||||||
export default function MdxTitle(
|
|
||||||
props: HTMLAttributes<HTMLHeadingElement>,
|
|
||||||
): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Title
|
|
||||||
{...props}
|
|
||||||
order={1}
|
|
||||||
id={String(props.children ?? '')
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll(' ', '-')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Flex, Button, ActionIcon, Group } from '@mantine/core'
|
|
||||||
import { IconBrandGithub, IconBook } from '@tabler/icons-react'
|
|
||||||
import ThemeBtn from '../theme-btn'
|
|
||||||
import { Link } from 'tuono'
|
|
||||||
|
|
||||||
export default function Actions(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Flex gap={8}>
|
|
||||||
<Button
|
|
||||||
href="/documentation"
|
|
||||||
component={Link}
|
|
||||||
size="compact-lg"
|
|
||||||
rightSection={<IconBook />}
|
|
||||||
autoContrast
|
|
||||||
>
|
|
||||||
Get started
|
|
||||||
</Button>
|
|
||||||
<Group gap={8} visibleFrom="sm">
|
|
||||||
<ActionIcon
|
|
||||||
variant="default"
|
|
||||||
size="lg"
|
|
||||||
aria-label="Check the project on github"
|
|
||||||
href="https://github.com/Valerioageno/tuono"
|
|
||||||
target="_blank"
|
|
||||||
component="a"
|
|
||||||
>
|
|
||||||
<IconBrandGithub />
|
|
||||||
</ActionIcon>
|
|
||||||
<ThemeBtn />
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Navbar from './navbar'
|
|
||||||
|
|
||||||
export default Navbar
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { AppShell, Burger, Button, Flex } from '@mantine/core'
|
|
||||||
import { Link, useRouter } from 'tuono'
|
|
||||||
import Actions from './actions'
|
|
||||||
|
|
||||||
interface NavbarProps {
|
|
||||||
opened: boolean
|
|
||||||
toggle: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Navbar({ opened, toggle }: NavbarProps): JSX.Element {
|
|
||||||
const { pathname } = useRouter()
|
|
||||||
return (
|
|
||||||
<AppShell.Header p="sm">
|
|
||||||
<Flex justify="space-between">
|
|
||||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
|
||||||
Tuono
|
|
||||||
</Button>
|
|
||||||
<Flex align="center" gap={8}>
|
|
||||||
<Actions />
|
|
||||||
{pathname.startsWith('/documentation') && (
|
|
||||||
<Burger
|
|
||||||
opened={opened}
|
|
||||||
onClick={toggle}
|
|
||||||
hiddenFrom="sm"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</AppShell.Header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Sidebar from './sidebar'
|
|
||||||
|
|
||||||
export default Sidebar
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.link {
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { NavLink, type NavLinkProps } from '@mantine/core'
|
|
||||||
import { useState, type ReactNode } from 'react'
|
|
||||||
import { Link, useRouter } from 'tuono'
|
|
||||||
import { IconChevronRight } from '@tabler/icons-react'
|
|
||||||
|
|
||||||
import styles from './sidebar-link.module.css'
|
|
||||||
|
|
||||||
interface SidebarLinkProps {
|
|
||||||
label: string
|
|
||||||
href: string
|
|
||||||
onClick?: () => void
|
|
||||||
children?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SidebarLink(
|
|
||||||
props: SidebarLinkProps & NavLinkProps,
|
|
||||||
): JSX.Element {
|
|
||||||
const { pathname } = useRouter()
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(!!props.defaultOpened)
|
|
||||||
|
|
||||||
const internalProps = {
|
|
||||||
active: pathname === props.href,
|
|
||||||
className: styles.link,
|
|
||||||
rightSection: props.children && (
|
|
||||||
<IconChevronRight
|
|
||||||
size="1.2rem"
|
|
||||||
stroke={1.5}
|
|
||||||
className="mantine-rotate-rtl"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
setIsOpen((state) => !state)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
opened: isOpen,
|
|
||||||
autoContrast: true,
|
|
||||||
...props,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.href.startsWith('#')) {
|
|
||||||
return <NavLink component="button" {...internalProps} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <NavLink component={Link} {...internalProps} />
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { AppShell } from '@mantine/core'
|
|
||||||
import SidebarLink from './sidebar-link'
|
|
||||||
|
|
||||||
export default function Sidebar({ close }: { close: () => void }): JSX.Element {
|
|
||||||
return (
|
|
||||||
<AppShell.Navbar p="md">
|
|
||||||
<h3>Tutorial</h3>
|
|
||||||
<SidebarLink
|
|
||||||
href="/documentation/tutorial/intro"
|
|
||||||
label="Intro"
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
<h3>Documentation</h3>
|
|
||||||
<SidebarLink href="/documentation" label="Overview" onClick={close} />
|
|
||||||
<SidebarLink
|
|
||||||
href="/documentation/installation"
|
|
||||||
label="Installation"
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
<SidebarLink
|
|
||||||
href="/documentation/getting-started"
|
|
||||||
label="Getting started"
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
<SidebarLink
|
|
||||||
label="Routing"
|
|
||||||
href="/documentation/routing"
|
|
||||||
defaultOpened
|
|
||||||
onClick={close}
|
|
||||||
>
|
|
||||||
<SidebarLink
|
|
||||||
href="/documentation/routing/intro"
|
|
||||||
label="Project structure"
|
|
||||||
onClick={close}
|
|
||||||
/>
|
|
||||||
</SidebarLink>
|
|
||||||
<SidebarLink
|
|
||||||
label="Contributing"
|
|
||||||
href="/documentation/contributing"
|
|
||||||
onClick={close}
|
|
||||||
leftSection="✨"
|
|
||||||
/>
|
|
||||||
</AppShell.Navbar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import ThemeBtn from './theme-btn'
|
|
||||||
|
|
||||||
export default ThemeBtn
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
.icon {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
@mixin dark {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin light {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.light {
|
|
||||||
@mixin light {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin dark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
useMantineColorScheme,
|
|
||||||
useComputedColorScheme,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { IconSun, IconMoon } from '@tabler/icons-react'
|
|
||||||
import cx from 'clsx'
|
|
||||||
|
|
||||||
import classes from './theme-btn.module.css'
|
|
||||||
|
|
||||||
export default function ThemeBtn(): JSX.Element {
|
|
||||||
const { setColorScheme } = useMantineColorScheme()
|
|
||||||
const computedColorScheme = useComputedColorScheme('light', {
|
|
||||||
getInitialValueInEffect: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ActionIcon
|
|
||||||
onClick={() =>
|
|
||||||
setColorScheme(computedColorScheme === 'light' ? 'dark' : 'light')
|
|
||||||
}
|
|
||||||
variant="default"
|
|
||||||
size="lg"
|
|
||||||
aria-label="Toggle color scheme"
|
|
||||||
>
|
|
||||||
<IconSun className={cx(classes.icon, classes.light)} stroke={1.5} />
|
|
||||||
<IconMoon className={cx(classes.icon, classes.dark)} stroke={1.5} />
|
|
||||||
</ActionIcon>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
// declaration.d.ts
|
|
||||||
declare module '*.css'
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
import {
|
|
||||||
ColorSchemeScript,
|
|
||||||
createTheme,
|
|
||||||
MantineProvider,
|
|
||||||
AppShell,
|
|
||||||
} from '@mantine/core'
|
|
||||||
import { useDisclosure } from '@mantine/hooks'
|
|
||||||
import { Head, useRouter } from 'tuono'
|
|
||||||
import Navbar from '../components/navbar'
|
|
||||||
|
|
||||||
import '@mantine/core/styles.css'
|
|
||||||
import '@mantine/code-highlight/styles.css'
|
|
||||||
import Sidebar from '../components/sidebar'
|
|
||||||
|
|
||||||
interface RootRouteProps {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = createTheme({
|
|
||||||
primaryColor: 'violet',
|
|
||||||
primaryShade: { light: 6, dark: 9 },
|
|
||||||
fontFamily: 'Roboto',
|
|
||||||
respectReducedMotion: true,
|
|
||||||
fontSizes: {
|
|
||||||
// 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
||||||
xs: '16px',
|
|
||||||
sm: '16px',
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
dark: [
|
|
||||||
'#d5d7e0',
|
|
||||||
'#acaebf',
|
|
||||||
'#8c8fa3',
|
|
||||||
'#666980',
|
|
||||||
'#4d4f66',
|
|
||||||
'#34354a',
|
|
||||||
'#2b2c3d',
|
|
||||||
'#1d1e30',
|
|
||||||
'#0c0d21',
|
|
||||||
'#01010a',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
headings: {
|
|
||||||
sizes: {
|
|
||||||
h1: {
|
|
||||||
fontSize: '48px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
|
||||||
const [opened, { toggle }] = useDisclosure()
|
|
||||||
|
|
||||||
const { pathname } = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<ColorSchemeScript />
|
|
||||||
</Head>
|
|
||||||
<MantineProvider theme={theme}>
|
|
||||||
<AppShell
|
|
||||||
header={{ height: 60 }}
|
|
||||||
navbar={{
|
|
||||||
width: 300,
|
|
||||||
breakpoint: 'sm',
|
|
||||||
collapsed: { mobile: !opened },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Navbar opened={opened} toggle={toggle} />
|
|
||||||
{pathname.startsWith('/documentation') && <Sidebar close={toggle} />}
|
|
||||||
{children}
|
|
||||||
</AppShell>
|
|
||||||
</MantineProvider>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { ReactNode } from 'react'
|
|
||||||
import { AppShell, Container } from '@mantine/core'
|
|
||||||
import MdxProvider from '../../components/mdx-provider'
|
|
||||||
import EditPage from '../../components/edit-page'
|
|
||||||
|
|
||||||
interface RootRouteProps {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<AppShell.Main>
|
|
||||||
<Container component="article" p={20}>
|
|
||||||
<MdxProvider>{children}</MdxProvider>
|
|
||||||
<EditPage />
|
|
||||||
</Container>
|
|
||||||
</AppShell.Main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Contributing</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="✨ Contributing" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Contributing
|
|
||||||
|
|
||||||
## TL;DR
|
|
||||||
|
|
||||||
The project is massive - if you like it do consider to contribute!
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
The `tuono` project can mostly be split by the following sub-domains:
|
|
||||||
|
|
||||||
- The CLI
|
|
||||||
- The rust backend
|
|
||||||
- The react frontend
|
|
||||||
- The documentation website (which is written with tuono 🚀)
|
|
||||||
|
|
||||||
To check what are the knowledge requirement for each domain check the
|
|
||||||
[requirements](#requirements) section below.
|
|
||||||
|
|
||||||
Currently I'm keeping a private dashboard to prioritize new features and bug fixes but if you
|
|
||||||
want to propose something please open a new issue on github or reach me using
|
|
||||||
my email address [valerioageno@yahoo.it](mailto:valerioageno@yahoo.it). I'm also available
|
|
||||||
on twitter (X) DMs `@valerioageno`, [linkedin](https://www.linkedin.com/in/valerioageno)
|
|
||||||
and discord `@__v__v__`.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
It's not strictly required to know both React (& typescript) and Rust (even though it
|
|
||||||
would be a great nice to have).
|
|
||||||
|
|
||||||
Without taking into account specific cases we can mostly split the domain requirements by:
|
|
||||||
|
|
||||||
- The `CLI` needs Rust knowledge (even though a couple of scenario might need also Typescript)
|
|
||||||
- The Backend needs just `Rust`
|
|
||||||
- The Frontend needs just `React` & `Typescript`
|
|
||||||
- The documentation website needs just `React` & `Typescript` (or even less since most of the
|
|
||||||
code is markdown).
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Getting started</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="Getting started" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Getting started
|
|
||||||
|
|
||||||
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
|
|
||||||
|
|
||||||
> ☝️ Check the [installation](/documentation/installation) page if you haven't installed the
|
|
||||||
> `tuono` CLI yet.
|
|
||||||
|
|
||||||
To list all the available commands run `tuono -h`.
|
|
||||||
|
|
||||||
To create a new project run `tuono new [NAME]` (optionally you can pass the --template (or -t) flag - check the examples folder).
|
|
||||||
|
|
||||||
Then to run the local development environment run inside the project folder `tuono dev`
|
|
||||||
|
|
||||||
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets and to set the server project in the production mode.
|
|
||||||
|
|
||||||
Now to execute it just run `cargo run --release`.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Documentation</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs from '../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs></Breadcrumbs>
|
|
||||||
|
|
||||||
# Tuono
|
|
||||||
|
|
||||||
**Tuono is a fullstack framework for building React applications using Rust as
|
|
||||||
backend.**
|
|
||||||
|
|
||||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/). Why Tuono? Just a badass name.
|
|
||||||
|
|
||||||
## Introduction
|
|
||||||
|
|
||||||
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be fullstack right? (no)**
|
|
||||||
|
|
||||||
Tuono is a fullstack React framework with the server side written in Rust.
|
|
||||||
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
|
|
||||||
React is still React - it is just superpowered.
|
|
||||||
|
|
||||||
**Rust is an hard language then writing server side code is hard as well right? (no again)**
|
|
||||||
|
|
||||||
Tuono provides a collection of utilities to handle the server side code seamlessly with
|
|
||||||
the React code. Each server side route is managed with a separate file alongside the React route.
|
|
||||||
The routing is handled by Tuono based on the files defined within the `./src/routes` directory.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
Some of its features include:
|
|
||||||
|
|
||||||
- 🟦 Full typescript support
|
|
||||||
- 🌐 File system based routing
|
|
||||||
- 🔥 Hot Module Reload
|
|
||||||
- 🍭 SCSS/CSS modules
|
|
||||||
- 🧬 Server Side Rendering
|
|
||||||
- 🧵 Multi thread backend
|
|
||||||
- ⌨️ MDX support
|
|
||||||
- ⚙️ Build optimizations
|
|
||||||
- Custom APIs\*
|
|
||||||
- Image optimization\*
|
|
||||||
- Server streamed content\*
|
|
||||||
|
|
||||||
> \*development in progress
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
import Breadcrumbs, { Element } from '../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Installation</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="Installation" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
`Tuono` is a development environment built in rust and typescript that outputs a website written with both the
|
|
||||||
languages. Since that you need the following tools installed on your computer to work with it:
|
|
||||||
|
|
||||||
- `Rust` - The rust programming language toolchain (Go [here](https://rustup.rs/) for installing both `rust` and `cargo`)
|
|
||||||
- `Cargo` - The rust package manager
|
|
||||||
- `NodeJs` - The Javascript runtime (Go [here](https://nodejs.org/en/download/package-manager) for installing both `nodejs` and `npm`)
|
|
||||||
- `npm` or `yarn` or `pnpm` - A javascript package manager
|
|
||||||
|
|
||||||
> NodeJs is needed just for the development environment. The final output will run on a rust server.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
The react/rust fullstack framework
|
|
||||||
|
|
||||||
Usage: tuono <COMMAND>
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
dev Start the development environment
|
|
||||||
build Build the production assets
|
|
||||||
new Scaffold a new project
|
|
||||||
help Print this message or the help of the given subcommand(s)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
-h, --help Print help
|
|
||||||
-V, --version Print version
|
|
||||||
```
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Routing</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="Routing" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Routing index
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Routing</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="Routing" href="/documentation/routing" />
|
|
||||||
<Element label="Project structure" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Project structure
|
|
||||||
|
|
||||||
Todo
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - Tutorial intro</title>
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
import Breadcrumbs, { Element } from '../../../components/breadcrumbs'
|
|
||||||
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Element label="Tutorial" />
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
# Tutorial
|
|
||||||
|
|
||||||
You can find the tutorial [here](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
|
||||||
|
|
||||||
Other tasks have the priority right now. The tutorial will be ported here as soon as possible.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Head } from 'tuono'
|
|
||||||
import Hero from '../components/hero'
|
|
||||||
|
|
||||||
export default function IndexPage(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Tuono - The react/rust fullstack framework</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="The technologies we love seamessly working together to unleash the highest web performance ever met on react"
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<Hero />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
+29
-13
@@ -1,18 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tuono"
|
name = "tuono"
|
||||||
version = "0.9.2"
|
version = "0.19.7"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||||
description = "The react/rust fullstack framework"
|
description = "Superfast React fullstack framework"
|
||||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
homepage = "https://tuono.dev"
|
||||||
repository = "https://github.com/Valerioageno/tuono"
|
keywords = ["react", "typescript", "fullstack", "web", "ssr"]
|
||||||
|
repository = "https://github.com/tuono-labs/tuono"
|
||||||
readme = "../../README.md"
|
readme = "../../README.md"
|
||||||
license-file = "../../LICENSE.md"
|
license-file = "../../LICENSE.md"
|
||||||
categories = ["web-programming"]
|
categories = ["web-programming"]
|
||||||
include = [
|
include = ["src/**/*.rs", "templates/*", "Cargo.toml"]
|
||||||
"src/*.rs",
|
|
||||||
"Cargo.toml"
|
|
||||||
]
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "tuono"
|
name = "tuono"
|
||||||
@@ -20,15 +18,33 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.4", features = ["derive", "cargo"] }
|
clap = { version = "4.5.4", features = ["derive", "cargo"] }
|
||||||
watchexec = "4.0.0"
|
syn = { version = "2.0.100", features = ["full"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = {version = "0.3.19", features = ["env-filter"]}
|
||||||
miette = "7.2.0"
|
miette = "7.2.0"
|
||||||
watchexec-signals = "3.0.0"
|
|
||||||
|
colored = "3.0.0"
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
watchexec = "5.0.0"
|
||||||
|
watchexec-signals = "4.0.0"
|
||||||
|
watchexec-events = "4.0.0"
|
||||||
|
watchexec-supervisor = "3.0.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1.0.202", features = ["derive"] }
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
watchexec-supervisor = "2.0.0"
|
|
||||||
glob = "0.3.1"
|
glob = "0.3.1"
|
||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
fs_extra = "1.3.0"
|
fs_extra = "1.3.0"
|
||||||
|
http = "1.1.0"
|
||||||
|
tuono_internal = {path = "../tuono_internal", version = "0.19.7"}
|
||||||
|
spinners = "4.1.1"
|
||||||
|
console = "0.16.0"
|
||||||
|
convert_case = "0.8.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
wiremock = "0.6.2"
|
||||||
|
tempfile = "3.14.0"
|
||||||
|
assert_cmd = "2.0.16"
|
||||||
|
serial_test = "3.0.0"
|
||||||
|
|
||||||
|
|||||||
+254
-37
@@ -1,29 +1,65 @@
|
|||||||
use glob::glob;
|
use crate::mode::Mode;
|
||||||
use glob::GlobError;
|
use crate::route::Route;
|
||||||
use std::collections::{hash_map::Entry, HashMap};
|
use glob::{GlobError, glob};
|
||||||
|
use http::Method;
|
||||||
|
use std::collections::hash_set::HashSet;
|
||||||
|
use std::collections::{HashMap, hash_map::Entry};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Child;
|
use std::process::Child;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
use tracing::error;
|
||||||
use crate::route::Route;
|
use tuono_internal::config::Config;
|
||||||
|
|
||||||
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
|
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 struct App {
|
||||||
pub route_map: HashMap<String, Route>,
|
pub route_map: HashMap<String, Route>,
|
||||||
pub base_path: PathBuf,
|
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 {
|
impl App {
|
||||||
pub fn new() -> Self {
|
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 {
|
let mut app = App {
|
||||||
route_map: HashMap::new(),
|
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();
|
app.collect_routes();
|
||||||
@@ -31,19 +67,32 @@ impl App {
|
|||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_routes(&mut self) {
|
pub fn collect_routes(&mut self) {
|
||||||
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
|
glob(
|
||||||
.expect("Failed to read glob pattern")
|
self.base_path
|
||||||
.for_each(|entry| {
|
.join("src/routes/**/*.*")
|
||||||
if self.should_collect_route(&entry) {
|
.to_str()
|
||||||
self.collect_route(entry)
|
.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 {
|
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
|
||||||
let file_extension = entry.as_ref().unwrap().extension().unwrap();
|
let file_extension = entry
|
||||||
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
|
.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) {
|
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
|
||||||
return false;
|
return false;
|
||||||
@@ -56,17 +105,23 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
||||||
let entry = path_buf.unwrap();
|
let entry = path_buf.expect("Failed to read glob path");
|
||||||
let base_path_str = self.base_path.to_str().unwrap();
|
|
||||||
|
let base_path_str = self
|
||||||
|
.base_path
|
||||||
|
.to_str()
|
||||||
|
.expect("Failed to read as str base_path");
|
||||||
let path = entry
|
let path = entry
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.expect("Failed to read entry as str")
|
||||||
.replace(&format!("{base_path_str}/src/routes"), "")
|
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
|
||||||
|
// Cleanup windows paths
|
||||||
|
.replace("\\", "/")
|
||||||
.replace(".rs", "")
|
.replace(".rs", "")
|
||||||
.replace(".mdx", "")
|
.replace(".mdx", "")
|
||||||
.replace(".tsx", "");
|
.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()) {
|
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||||
let mut route = Route::new(path);
|
let mut route = Route::new(path);
|
||||||
route.update_axum_info();
|
route.update_axum_info();
|
||||||
@@ -87,10 +142,53 @@ impl App {
|
|||||||
self.route_map.iter().any(|(_, route)| route.is_dynamic)
|
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 {vite_port} is not already in use by another process or application."
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_react_prod(&self) {
|
pub fn build_react_prod(&self) {
|
||||||
Command::new("./node_modules/.bin/tuono-build-prod")
|
if !Path::new(BUILD_JS_SCRIPT).exists() {
|
||||||
.output()
|
error!("Failed to find the build script. Please run `npm install`");
|
||||||
.expect("Failed to build the react source");
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Command::new(BUILD_JS_SCRIPT).output().unwrap_or_else(|_| {
|
||||||
|
error!("Failed to build the react source");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
error!("Failed to build the react source");
|
||||||
|
error!("Error: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_rust_server(&self) -> Child {
|
pub fn run_rust_server(&self) -> Child {
|
||||||
@@ -102,24 +200,93 @@ impl App {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to run the rust server")
|
.expect("Failed to run the rust server")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_tuono_config(&mut self) -> io::Result<std::process::Output> {
|
||||||
|
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();
|
||||||
|
|
||||||
|
match Config::get() {
|
||||||
|
Ok(config) => self.config = Some(config),
|
||||||
|
Err(error) => {
|
||||||
|
match error.kind() {
|
||||||
|
io::ErrorKind::NotFound => eprintln!(
|
||||||
|
"Failed to read config. Please run `npm install` to generate automatically."
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
error!("Failed to read config with the following error:");
|
||||||
|
error!("{}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_collect_routes() {
|
fn should_collect_routes() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/index.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/index.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/posts/[post].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/posts/UPPERCASE.rs",
|
||||||
|
"/home/user/Documents/tuono/src/routes/sitemap.xml.rs",
|
||||||
];
|
];
|
||||||
|
|
||||||
routes
|
routes
|
||||||
@@ -131,7 +298,13 @@ mod tests {
|
|||||||
("/about", "about"),
|
("/about", "about"),
|
||||||
("/posts/index", "posts_index"),
|
("/posts/index", "posts_index"),
|
||||||
("/posts/[post]", "posts_dyn_post"),
|
("/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"),
|
("/posts/UPPERCASE", "posts_uppercase"),
|
||||||
|
("/sitemap.xml", "sitemap_dot_xml"),
|
||||||
];
|
];
|
||||||
|
|
||||||
results.into_iter().for_each(|(path, module_import)| {
|
results.into_iter().for_each(|(path, module_import)| {
|
||||||
@@ -151,8 +324,25 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_create_multi_level_axum_paths() {
|
fn should_create_multi_level_axum_paths() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||||
@@ -170,14 +360,14 @@ mod tests {
|
|||||||
("/about", "/about"),
|
("/about", "/about"),
|
||||||
("/posts/index", "/posts"),
|
("/posts/index", "/posts"),
|
||||||
("/posts/any-post", "/posts/any-post"),
|
("/posts/any-post", "/posts/any-post"),
|
||||||
("/posts/[post]", "/posts/:post"),
|
("/posts/[post]", "/posts/{post}"),
|
||||||
];
|
];
|
||||||
|
|
||||||
results.into_iter().for_each(|(path, expected_path)| {
|
results.into_iter().for_each(|(path, expected_path)| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.route_map
|
app.route_map
|
||||||
.get(path)
|
.get(path)
|
||||||
.unwrap()
|
.expect("Failed to get route path")
|
||||||
.axum_info
|
.axum_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -213,8 +403,8 @@ mod tests {
|
|||||||
app.base_path = "/home/user/Documents/tuono".into();
|
app.base_path = "/home/user/Documents/tuono".into();
|
||||||
|
|
||||||
let routes = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/__root.tsx",
|
"/home/user/Documents/tuono/src/routes/__layout.tsx",
|
||||||
"/home/user/Documents/tuono/src/routes/posts/__root.tsx",
|
"/home/user/Documents/tuono/src/routes/posts/__layout.tsx",
|
||||||
];
|
];
|
||||||
|
|
||||||
routes.into_iter().for_each(|route| {
|
routes.into_iter().for_each(|route| {
|
||||||
@@ -229,8 +419,23 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_parse_routes_with_server_handler() {
|
fn should_correctly_parse_routes_with_server_handler() {
|
||||||
let mut app = App::new();
|
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 = [
|
let routes = [
|
||||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||||
"/home/user/Documents/tuono/src/routes/about.tsx",
|
"/home/user/Documents/tuono/src/routes/about.tsx",
|
||||||
@@ -247,9 +452,21 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|(path, expected_has_server_handler)| {
|
.for_each(|(path, expected_has_server_handler)| {
|
||||||
if 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 {
|
} 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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-77
@@ -1,15 +1,9 @@
|
|||||||
use fs_extra::dir::{copy, CopyOptions};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::thread::sleep;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use tracing::{Level, span};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::commands::{build, dev, new};
|
||||||
use crate::mode::Mode;
|
use crate::mode::Mode;
|
||||||
use crate::scaffold_project;
|
use crate::source_builder::SourceBuilder;
|
||||||
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
|
||||||
use crate::watch;
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
enum Actions {
|
enum Actions {
|
||||||
@@ -20,6 +14,10 @@ enum Actions {
|
|||||||
#[arg(short, long = "static")]
|
#[arg(short, long = "static")]
|
||||||
/// Statically generate the website HTML
|
/// Statically generate the website HTML
|
||||||
ssg: bool,
|
ssg: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
/// Prevent to export the js assets
|
||||||
|
no_js_emit: bool,
|
||||||
},
|
},
|
||||||
/// Scaffold a new project
|
/// Scaffold a new project
|
||||||
New {
|
New {
|
||||||
@@ -29,96 +27,55 @@ enum Actions {
|
|||||||
/// examples
|
/// examples
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
template: Option<String>,
|
template: Option<String>,
|
||||||
|
/// Load the latest commit available on the main branch
|
||||||
|
#[arg(long)]
|
||||||
|
head: Option<bool>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about = "The react/rust fullstack framework")]
|
#[command(version, about = "The React/Rust full-stack framework")]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: Actions,
|
action: Actions,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
|
||||||
check_tuono_folder()?;
|
|
||||||
bundle_axum_source(mode)?;
|
|
||||||
create_client_entry_files()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn app() -> std::io::Result<()> {
|
pub fn app() -> std::io::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
match args.action {
|
match args.action {
|
||||||
Actions::Dev => {
|
Actions::Dev => {
|
||||||
init_tuono_folder(Mode::Dev)?;
|
let span = span!(Level::TRACE, "DEV");
|
||||||
watch::watch().unwrap();
|
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
|
let mut source_builder = SourceBuilder::new(Mode::Dev)?;
|
||||||
|
|
||||||
|
source_builder.base_build()?;
|
||||||
|
|
||||||
|
source_builder.app.check_server_availability(Mode::Dev);
|
||||||
|
|
||||||
|
dev::watch(source_builder).unwrap();
|
||||||
}
|
}
|
||||||
Actions::Build { ssg } => {
|
Actions::Build { ssg, no_js_emit } => {
|
||||||
init_tuono_folder(Mode::Prod)?;
|
let span = span!(Level::TRACE, "BUILD");
|
||||||
let app = App::new();
|
|
||||||
|
|
||||||
if ssg && app.has_dynamic_routes() {
|
let _guard = span.enter();
|
||||||
// TODO: allow dynamic routes static generation
|
|
||||||
println!("Cannot statically build dynamic routes");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
app.build_react_prod();
|
let mut source_builder = SourceBuilder::new(Mode::Prod)?;
|
||||||
|
source_builder.base_build()?;
|
||||||
if ssg {
|
build::build(source_builder.app, ssg, no_js_emit);
|
||||||
println!("SSG: generation started");
|
|
||||||
|
|
||||||
let static_dir = PathBuf::from("out/static");
|
|
||||||
|
|
||||||
if static_dir.is_dir() {
|
|
||||||
std::fs::remove_dir_all(&static_dir)
|
|
||||||
.expect("Failed to clear the out/static folder");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::create_dir(&static_dir).expect("Failed to create static output dir");
|
|
||||||
|
|
||||||
copy(
|
|
||||||
"./out/client",
|
|
||||||
static_dir,
|
|
||||||
&CopyOptions::new().overwrite(true).content_only(true),
|
|
||||||
)
|
|
||||||
.expect("Failed to clone assets into static output folder");
|
|
||||||
|
|
||||||
let mut rust_server = app.run_rust_server();
|
|
||||||
|
|
||||||
let reqwest = reqwest::blocking::Client::builder()
|
|
||||||
.user_agent("")
|
|
||||||
.build()
|
|
||||||
.expect("Failed to build reqwest client");
|
|
||||||
|
|
||||||
// Wait for server
|
|
||||||
let mut is_server_ready = false;
|
|
||||||
|
|
||||||
while !is_server_ready {
|
|
||||||
if reqwest.get("http://localhost:3000").send().is_ok() {
|
|
||||||
is_server_ready = true
|
|
||||||
}
|
|
||||||
// TODO: add maximum tries
|
|
||||||
sleep(Duration::from_secs(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, route) in app.route_map {
|
|
||||||
route.save_ssg_html(&reqwest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close server
|
|
||||||
let _ = rust_server.kill();
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("Build successfully finished");
|
|
||||||
}
|
}
|
||||||
Actions::New {
|
Actions::New {
|
||||||
folder_name,
|
folder_name,
|
||||||
template,
|
template,
|
||||||
|
head,
|
||||||
} => {
|
} => {
|
||||||
scaffold_project::create_new_project(folder_name, template);
|
let span = span!(Level::TRACE, "NEW");
|
||||||
|
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
|
new::create_new_project(folder_name, template, head);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use fs_extra::dir::{CopyOptions, copy};
|
||||||
|
use spinners::{Spinner, Spinners};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{error, trace};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::mode::Mode;
|
||||||
|
|
||||||
|
fn exit_gracefully_with_error(msg: &str) -> ! {
|
||||||
|
error!(msg);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(mut app: App, ssg: bool, no_js_emit: bool) {
|
||||||
|
if no_js_emit {
|
||||||
|
println!("Rust build successfully finished");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ssg && app.has_dynamic_routes() {
|
||||||
|
// TODO: allow dynamic routes static generation
|
||||||
|
println!("Cannot statically build dynamic routes");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.build_tuono_config()
|
||||||
|
.unwrap_or_else(|_| exit_gracefully_with_error("Failed to build tuono.config.ts"));
|
||||||
|
|
||||||
|
let mut app_build_spinner = Spinner::new(Spinners::Dots, "Building app...".into());
|
||||||
|
|
||||||
|
app.check_server_availability(Mode::Prod);
|
||||||
|
|
||||||
|
app.build_react_prod();
|
||||||
|
|
||||||
|
// Remove the spinner
|
||||||
|
app_build_spinner.stop_with_message("\u{2705}Build completed".into());
|
||||||
|
|
||||||
|
if ssg {
|
||||||
|
let mut app_build_static_spinner =
|
||||||
|
Spinner::new(Spinners::Dots, "Static site generation".into());
|
||||||
|
|
||||||
|
let mut exit_gracefully_with_error = |msg: &str| -> ! {
|
||||||
|
app_build_static_spinner.stop_with_message("\u{274C} Build failed\n".into());
|
||||||
|
exit_gracefully_with_error(msg)
|
||||||
|
};
|
||||||
|
|
||||||
|
let static_dir = PathBuf::from("out/static");
|
||||||
|
|
||||||
|
if static_dir.is_dir() {
|
||||||
|
std::fs::remove_dir_all(&static_dir).unwrap_or_else(|_| {
|
||||||
|
exit_gracefully_with_error("Failed to clear the out/static folder")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir(&static_dir)
|
||||||
|
.unwrap_or_else(|_| exit_gracefully_with_error("Failed to create static output dir"));
|
||||||
|
|
||||||
|
copy(
|
||||||
|
"./out/client",
|
||||||
|
static_dir,
|
||||||
|
&CopyOptions::new().overwrite(true).content_only(true),
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
exit_gracefully_with_error("Failed to clone assets into static output folder")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
#[allow(clippy::zombie_processes)]
|
||||||
|
let mut rust_server = app.run_rust_server();
|
||||||
|
|
||||||
|
let mut exit_and_shut_server = |msg: &str| -> ! {
|
||||||
|
_ = rust_server.kill();
|
||||||
|
exit_gracefully_with_error(msg)
|
||||||
|
};
|
||||||
|
|
||||||
|
let reqwest_client = reqwest::blocking::Client::builder()
|
||||||
|
.user_agent("")
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| exit_and_shut_server("Failed to build reqwest client"));
|
||||||
|
|
||||||
|
// Wait for server
|
||||||
|
let mut is_server_ready = false;
|
||||||
|
let config = app.config.as_ref().unwrap();
|
||||||
|
|
||||||
|
while !is_server_ready {
|
||||||
|
trace!("Checking server availability");
|
||||||
|
|
||||||
|
let server_url = match &config.server.origin {
|
||||||
|
Some(origin) => origin.clone(),
|
||||||
|
None => format!("http://{}:{}", config.server.host, config.server.port),
|
||||||
|
};
|
||||||
|
|
||||||
|
if reqwest_client.get(&server_url).send().is_ok() {
|
||||||
|
is_server_ready = true;
|
||||||
|
} else {
|
||||||
|
trace!("Server not ready yet. Sleeping for 1 second");
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Server is ready, starting static site generation");
|
||||||
|
|
||||||
|
for (_, route) in app.route_map {
|
||||||
|
if let Err(msg) = route.save_ssg_file(&reqwest_client) {
|
||||||
|
exit_and_shut_server(&msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close server
|
||||||
|
let _ = rust_server.kill();
|
||||||
|
|
||||||
|
app_build_static_spinner
|
||||||
|
.stop_with_message("\u{2705}Static site generation completed".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::error;
|
||||||
|
use tuono_internal::tuono_println;
|
||||||
|
use watchexec_events::Tag;
|
||||||
|
use watchexec_events::filekind::FileEventKind;
|
||||||
|
|
||||||
|
use crate::process_manager::{ProcessId, ProcessManager};
|
||||||
|
|
||||||
|
use miette::{IntoDiagnostic, Result};
|
||||||
|
use watchexec::Watchexec;
|
||||||
|
use watchexec_signals::Signal;
|
||||||
|
|
||||||
|
use crate::source_builder::SourceBuilder;
|
||||||
|
use console::Term;
|
||||||
|
use spinners::{Spinner, Spinners};
|
||||||
|
|
||||||
|
fn is_css_module(file_name: Cow<str>) -> bool {
|
||||||
|
static RE: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"^.*\.module\.(css|scss|sass|less|styl|stylus)$").unwrap());
|
||||||
|
RE.is_match(&file_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssr_reload_needed(path: &Path) -> bool {
|
||||||
|
let file_name_starts_with_env = path
|
||||||
|
.file_name()
|
||||||
|
.map(|f| f.to_string_lossy().starts_with(".env"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let file_path = path.to_string_lossy();
|
||||||
|
|
||||||
|
file_name_starts_with_env
|
||||||
|
|| file_path.ends_with("sx")
|
||||||
|
|| file_path.ends_with("mdx")
|
||||||
|
// When a CSS module is modified
|
||||||
|
// also the class names get modified.
|
||||||
|
|| is_css_module(file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::await_holding_lock,
|
||||||
|
reason = "At this point there is no other thread waiting for the lock"
|
||||||
|
)]
|
||||||
|
async unsafe fn start_all_processes(process_manager: Arc<Mutex<ProcessManager>>) {
|
||||||
|
if let Ok(mut pm) = process_manager.lock() {
|
||||||
|
pm.start_dev_processes().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_existing_env_files() -> Vec<String> {
|
||||||
|
if let Ok(dir) = fs::read_dir("./") {
|
||||||
|
dir.filter_map(|entry| entry.ok())
|
||||||
|
.filter(|entry| entry.file_name().to_string_lossy().starts_with(".env"))
|
||||||
|
.map(|entry| entry.path().to_string_lossy().into_owned())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn watch(source_builder: SourceBuilder) -> Result<()> {
|
||||||
|
let source_builder = RwLock::new(source_builder);
|
||||||
|
let term = Term::stdout();
|
||||||
|
let mut sp = Spinner::new(Spinners::Dots, "Starting dev server...".into());
|
||||||
|
|
||||||
|
let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
|
||||||
|
|
||||||
|
let env_files = detect_existing_env_files();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// It is safe to call this function because here
|
||||||
|
// only one thread is running and the lock is not
|
||||||
|
// needed by any other thread.
|
||||||
|
start_all_processes(process_manager.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the spinner
|
||||||
|
sp.stop();
|
||||||
|
_ = term.clear_line();
|
||||||
|
|
||||||
|
// Server address log for production
|
||||||
|
// is done on the server process.
|
||||||
|
if let Ok(builder) = source_builder.read() {
|
||||||
|
if let Ok(pm) = process_manager.lock() {
|
||||||
|
pm.log_server_address(builder.app.config.clone().unwrap_or_default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wx = Watchexec::new(move |mut action| {
|
||||||
|
let process_manager = process_manager.clone();
|
||||||
|
// if Ctrl-C is received, quit
|
||||||
|
if action.signals().any(|sig| sig == Signal::Interrupt) {
|
||||||
|
if let Ok(mut pm) = process_manager.lock() {
|
||||||
|
pm.abort_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
action.quit_gracefully(Signal::Quit, Duration::from_secs(30));
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The best way to trigger the processes is to use these
|
||||||
|
// flags.
|
||||||
|
// This because the same event can be triggered multiple times
|
||||||
|
// and we don't want to restart the process multiple times for the same
|
||||||
|
// purpose.
|
||||||
|
let mut should_reload_ssr_bundle = false;
|
||||||
|
let mut should_reload_rust_server = false;
|
||||||
|
let mut should_refresh_axum_source = false;
|
||||||
|
|
||||||
|
// Using HashSet to avoid duplicates
|
||||||
|
let mut paths_to_refresh_types: HashSet<PathBuf> = HashSet::new();
|
||||||
|
let mut removed_files_from_types: HashSet<PathBuf> = HashSet::new();
|
||||||
|
|
||||||
|
for event in action.events.iter() {
|
||||||
|
for event_type in event.tags.iter() {
|
||||||
|
if let Tag::FileEventKind(kind) = event_type {
|
||||||
|
match kind {
|
||||||
|
FileEventKind::Remove(_) => event.paths().for_each(|(path, _)| {
|
||||||
|
if path.extension().is_some_and(|ext| ext == "rs") {
|
||||||
|
removed_files_from_types.insert(path.to_path_buf());
|
||||||
|
should_refresh_axum_source = true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
FileEventKind::Modify(_) => event.paths().for_each(|(path, _)| {
|
||||||
|
if path.extension().is_some_and(|ext| ext == "rs") {
|
||||||
|
should_reload_rust_server = true;
|
||||||
|
paths_to_refresh_types.insert(path.to_path_buf());
|
||||||
|
}
|
||||||
|
if ssr_reload_needed(path) {
|
||||||
|
should_reload_ssr_bundle = true;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !paths_to_refresh_types.is_empty() {
|
||||||
|
if let Ok(mut builder) = source_builder.write() {
|
||||||
|
for path in paths_to_refresh_types {
|
||||||
|
// There is no need to check here if the `Type` trait is
|
||||||
|
// derived since it will be checked later by the TypeJar struct.
|
||||||
|
builder.refresh_typescript_file(path)
|
||||||
|
}
|
||||||
|
if builder.generate_typescript_file().is_err() {
|
||||||
|
error!("Failed to generate typescript file");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removed_files_from_types.is_empty() {
|
||||||
|
if let Ok(mut builder) = source_builder.write() {
|
||||||
|
for path in removed_files_from_types {
|
||||||
|
builder.remove_typescript_file(path);
|
||||||
|
}
|
||||||
|
if builder.generate_typescript_file().is_err() {
|
||||||
|
error!("Failed to generate typescript file");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_reload_rust_server || should_refresh_axum_source {
|
||||||
|
tuono_println!("Reloading...");
|
||||||
|
if should_refresh_axum_source {
|
||||||
|
if let Ok(mut builder) = source_builder.write() {
|
||||||
|
builder.app.collect_routes();
|
||||||
|
_ = builder.refresh_axum_source();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(mut pm) = process_manager.lock() {
|
||||||
|
pm.restart_process(ProcessId::RunRustDevServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_reload_ssr_bundle {
|
||||||
|
if let Ok(mut pm) = process_manager.lock() {
|
||||||
|
pm.restart_process(ProcessId::BuildReactSSRSrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// watch the current directory and all types of .env file
|
||||||
|
let mut paths_to_watch = vec!["./src".to_string()];
|
||||||
|
paths_to_watch.extend(env_files);
|
||||||
|
|
||||||
|
wx.config.pathset(paths_to_watch);
|
||||||
|
|
||||||
|
let _ = wx.main().await.into_diagnostic()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod build;
|
||||||
|
pub mod dev;
|
||||||
|
pub mod new;
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
use clap::crate_version;
|
||||||
|
use reqwest::blocking;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{self, File, OpenOptions, create_dir};
|
||||||
|
use std::io::{self, prelude::*};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
enum GithubFileType {
|
||||||
|
#[serde(rename = "blob")]
|
||||||
|
Blob,
|
||||||
|
#[serde(rename = "tree")]
|
||||||
|
Tree,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
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,
|
||||||
|
#[serde(rename(deserialize = "type"))]
|
||||||
|
element_type: GithubFileType,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
|
||||||
|
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>,
|
||||||
|
select_head: Option<bool>,
|
||||||
|
) {
|
||||||
|
let folder = folder_name.unwrap_or(".".to_string());
|
||||||
|
|
||||||
|
let github_api_base_url =
|
||||||
|
env::var("__INTERNAL_TUONO_TEST").unwrap_or("https://api.github.com".to_string());
|
||||||
|
|
||||||
|
let github_raw_base_url = env::var("__INTERNAL_TUONO_TEST")
|
||||||
|
.unwrap_or("https://raw.githubusercontent.com".to_string());
|
||||||
|
|
||||||
|
// In case of missing select the tuono example
|
||||||
|
let template = template.unwrap_or("tuono-app".to_string());
|
||||||
|
let client = blocking::Client::builder()
|
||||||
|
.user_agent("")
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| exit_with_error("Error: Failed to build request client"));
|
||||||
|
|
||||||
|
// 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, &github_api_base_url);
|
||||||
|
|
||||||
|
let res_tree = client
|
||||||
|
.get(tree_url)
|
||||||
|
.send()
|
||||||
|
.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_tree
|
||||||
|
.tree
|
||||||
|
.iter()
|
||||||
|
.filter(|GithubFile { path, .. }| path.starts_with(&format!("examples/{template}/")))
|
||||||
|
.collect::<Vec<&GithubFile>>();
|
||||||
|
|
||||||
|
if new_project_files.is_empty() {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
let folder_name = PathBuf::from(&folder);
|
||||||
|
let current_dir = env::current_dir().expect("Failed to get current working directory");
|
||||||
|
|
||||||
|
let folder_path = current_dir.join(folder_name);
|
||||||
|
|
||||||
|
create_directories(&new_project_files, &folder_path, &template)
|
||||||
|
.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, &github_raw_base_url);
|
||||||
|
|
||||||
|
let file_content = client
|
||||||
|
.get(url)
|
||||||
|
.send()
|
||||||
|
.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);
|
||||||
|
|
||||||
|
if let Err(err) = create_file(file_path, file_content) {
|
||||||
|
exit_with_error(&format!("Failed to create file: {err}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_package_json_version(&folder_path).expect("Failed to update package.json version");
|
||||||
|
update_cargo_toml_version(&folder_path).expect("Failed to update Cargo.toml version");
|
||||||
|
|
||||||
|
init_new_git_repo(&folder_path);
|
||||||
|
|
||||||
|
outro(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_raw_content_url(
|
||||||
|
select_head: Option<bool>,
|
||||||
|
cli_version: &str,
|
||||||
|
path: &String,
|
||||||
|
url: &str,
|
||||||
|
) -> String {
|
||||||
|
let tag = if select_head.unwrap_or(false) {
|
||||||
|
"/main"
|
||||||
|
} else {
|
||||||
|
&format!("/tuono-labs/tuono/v{cli_version}")
|
||||||
|
};
|
||||||
|
format!("{url}{tag}/{path}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_tree_url(
|
||||||
|
select_head: Option<bool>,
|
||||||
|
client: &Client,
|
||||||
|
cli_version: &str,
|
||||||
|
url: &str,
|
||||||
|
) -> String {
|
||||||
|
if select_head.unwrap_or(false) {
|
||||||
|
format!("{url}/repos/tuono-labs/tuono/git/trees/main?recursive=1")
|
||||||
|
} else {
|
||||||
|
// This string does not include the "v" version prefix
|
||||||
|
let res_tag = client
|
||||||
|
.get(format!(
|
||||||
|
"{url}/repos/tuono-labs/tuono/git/ref/tags/v{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!(
|
||||||
|
"{url}/repos/tuono-labs/tuono/git/trees/{}?recursive=1",
|
||||||
|
res_tag.object.sha
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_directories(
|
||||||
|
new_project_files: &[&GithubFile],
|
||||||
|
folder_path: &Path,
|
||||||
|
template: &String,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
for GithubFile {
|
||||||
|
element_type, path, ..
|
||||||
|
} in new_project_files.iter()
|
||||||
|
{
|
||||||
|
if let GithubFileType::Tree = element_type {
|
||||||
|
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
|
||||||
|
|
||||||
|
let dir_path = folder_path.join(&path);
|
||||||
|
if let Err(e) = create_dir(&dir_path) {
|
||||||
|
eprintln!("Failed to create directory {}: {}", dir_path.display(), e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
.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)
|
||||||
|
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open package.json: {err}")));
|
||||||
|
|
||||||
|
file.write_all(package_json.as_bytes())
|
||||||
|
.unwrap_or_else(|err| exit_with_error(&format!("Failed to write to package.json: {err}")));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.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)
|
||||||
|
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open Cargo.toml: {err}")));
|
||||||
|
|
||||||
|
file.write_all(cargo_toml.as_bytes())
|
||||||
|
.unwrap_or_else(|err| exit_with_error(&format!("Failed to write to Cargo.toml: {err}")));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_new_git_repo(folder_path: &Path) {
|
||||||
|
if let Ok(output) = Command::new("git").arg("init").arg(folder_path).output() {
|
||||||
|
if !output.status.success() {
|
||||||
|
trace!("Failed to initialise a new git repo")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trace!("Failed to initialise a new git repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outro(folder_name: String) {
|
||||||
|
println!("Success! 🎉");
|
||||||
|
|
||||||
|
if folder_name != "." {
|
||||||
|
println!("\nGo to the project directory:");
|
||||||
|
println!("cd {folder_name}/");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nInstall the dependencies:");
|
||||||
|
println!("npm install");
|
||||||
|
|
||||||
|
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!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
"http://localhost:3000", "main", "examples/tuono-app"
|
||||||
|
);
|
||||||
|
let generated = generate_raw_content_url(
|
||||||
|
Some(true),
|
||||||
|
crate_version!(),
|
||||||
|
&String::from("examples/tuono-app"),
|
||||||
|
"http://localhost:3000",
|
||||||
|
);
|
||||||
|
assert_eq!(expected, generated)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_valid_content_url_from_cli_version() {
|
||||||
|
let expected = format!(
|
||||||
|
"{}/{}/{}",
|
||||||
|
"http://localhost:3000",
|
||||||
|
&format!("tuono-labs/tuono/v{}", crate_version!()),
|
||||||
|
"examples/tuono-app"
|
||||||
|
);
|
||||||
|
let generated = generate_raw_content_url(
|
||||||
|
Some(false),
|
||||||
|
crate_version!(),
|
||||||
|
&String::from("examples/tuono-app"),
|
||||||
|
"http://localhost:3000",
|
||||||
|
);
|
||||||
|
assert_eq!(expected, generated)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
|
//! ## 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;
|
mod app;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
mod commands;
|
||||||
mod mode;
|
mod mode;
|
||||||
|
mod process_manager;
|
||||||
mod route;
|
mod route;
|
||||||
mod scaffold_project;
|
|
||||||
mod source_builder;
|
mod source_builder;
|
||||||
mod watch;
|
mod symbols;
|
||||||
|
mod typescript;
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
|
use tracing::error;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
use tuono::cli::app;
|
use tuono::cli::app;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
app().expect("Failed to start the CLI")
|
tracing_subscriber::fmt()
|
||||||
|
// Time not needed since the execution is synchronous
|
||||||
|
.without_time()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
if let Err(e) = app() {
|
||||||
|
// Generic error.
|
||||||
|
// Recoverable errors should be managed locally
|
||||||
|
error!("Failed to run the tuono CLI: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#[derive(PartialEq, Eq)]
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
Prod,
|
Prod,
|
||||||
Dev,
|
Dev,
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use clap::crate_version;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
|
use colored::Colorize;
|
||||||
|
|
||||||
|
use tracing::trace;
|
||||||
|
use tuono_internal::config::Config;
|
||||||
|
use tuono_internal::tuono_println;
|
||||||
|
use watchexec_supervisor::command::{Command, Program};
|
||||||
|
use watchexec_supervisor::job::{Job, start_job};
|
||||||
|
|
||||||
|
#[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";
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum ProcessId {
|
||||||
|
WatchReactSrc,
|
||||||
|
RunRustDevServer,
|
||||||
|
BuildRustSrc,
|
||||||
|
BuildReactSSRSrc,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This struct manages all the processes spawned by the dev server
|
||||||
|
// That are not the dev server itself
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProcessManager {
|
||||||
|
processes: HashMap<ProcessId, (Job, JoinHandle<()>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
trace!("Creating process manager");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
let mut processes = HashMap::new();
|
||||||
|
processes.insert(
|
||||||
|
ProcessId::WatchReactSrc,
|
||||||
|
start_supervisor_job(DEV_WATCH_BIN_SRC, Vec::new()),
|
||||||
|
);
|
||||||
|
|
||||||
|
processes.insert(
|
||||||
|
ProcessId::RunRustDevServer,
|
||||||
|
start_supervisor_job("cargo", vec!["run", "-q"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
processes.insert(
|
||||||
|
ProcessId::BuildRustSrc,
|
||||||
|
start_supervisor_job("cargo", vec!["build", "-q"]),
|
||||||
|
);
|
||||||
|
|
||||||
|
processes.insert(
|
||||||
|
ProcessId::BuildReactSSRSrc,
|
||||||
|
start_supervisor_job(DEV_SSR_BIN_SRC, Vec::new()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self { processes }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_process(&mut self, id: ProcessId) {
|
||||||
|
trace!("start process {:?}", id);
|
||||||
|
if let Some((job, _)) = self.processes.get(&id) {
|
||||||
|
job.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start all the processes needed for the dev server
|
||||||
|
pub async fn start_dev_processes(&mut self) {
|
||||||
|
trace!("Starting dev processes");
|
||||||
|
self.start_process(ProcessId::WatchReactSrc);
|
||||||
|
self.start_process(ProcessId::BuildRustSrc);
|
||||||
|
self.start_process(ProcessId::BuildReactSSRSrc);
|
||||||
|
|
||||||
|
self.wait_for_process(ProcessId::BuildRustSrc).await;
|
||||||
|
self.wait_for_process(ProcessId::BuildReactSSRSrc).await;
|
||||||
|
|
||||||
|
// This needs to start after having built the rust and react sources
|
||||||
|
self.start_process(ProcessId::RunRustDevServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_server_address(&self, config: Config) {
|
||||||
|
let server_address = format!("{}:{}", config.server.host, config.server.port);
|
||||||
|
// 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://{server_address}");
|
||||||
|
|
||||||
|
println!();
|
||||||
|
tuono_println!("⚡ Tuono v{}", crate_version!());
|
||||||
|
|
||||||
|
tuono_println!("Development server at: {}\n", server_base_url.blue().bold());
|
||||||
|
if let Some(origin) = config.server.origin {
|
||||||
|
tuono_println!("Origin: {}\n", origin.blue().bold());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restart_process(&mut self, id: ProcessId) {
|
||||||
|
trace!("Restarting process {:?}", id);
|
||||||
|
if let Some((job, _)) = self.processes.get(&id) {
|
||||||
|
job.stop();
|
||||||
|
job.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait_for_process(&mut self, id: ProcessId) {
|
||||||
|
trace!("Waiting for process {:?}", id);
|
||||||
|
if let Some((job, _)) = self.processes.get(&id) {
|
||||||
|
job.to_wait().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abort_all(&mut self) {
|
||||||
|
trace!("Aborting all processes");
|
||||||
|
for (_, (job, handle)) in self.processes.iter() {
|
||||||
|
job.stop();
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_supervisor_job(prog: &str, args: Vec<&str>) -> (Job, JoinHandle<()>) {
|
||||||
|
start_job(Arc::new(Command {
|
||||||
|
program: Program::Exec {
|
||||||
|
prog: prog.into(),
|
||||||
|
args: args.into_iter().map(|arg| arg.to_string()).collect(),
|
||||||
|
},
|
||||||
|
options: Default::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
+202
-45
@@ -1,17 +1,20 @@
|
|||||||
use fs_extra::dir::create_all;
|
use fs_extra::dir::create_all;
|
||||||
|
use http::Method;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::blocking::Client;
|
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
fn has_dynamic_path(route: &str) -> bool {
|
fn has_dynamic_path(route: &str) -> bool {
|
||||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||||
regex.is_match(route)
|
regex.is_match(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct AxumInfo {
|
pub struct AxumInfo {
|
||||||
// Path for importing the module
|
// Path for importing the module
|
||||||
pub module_import: String,
|
pub module_import: String,
|
||||||
@@ -20,44 +23,120 @@ pub struct AxumInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AxumInfo {
|
impl AxumInfo {
|
||||||
pub fn new(path: String) -> Self {
|
pub fn new(route: &Route) -> Self {
|
||||||
// Remove first slash
|
// Remove first slash
|
||||||
let mut module = path.chars();
|
let mut module = route.path.chars();
|
||||||
module.next();
|
module.next();
|
||||||
|
|
||||||
let axum_route = path.replace("/index", "");
|
let axum_route = if route.path.ends_with("/index") {
|
||||||
|
route.path.replace("/index", "")
|
||||||
|
} else {
|
||||||
|
route.path.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let module_import = module
|
||||||
|
.as_str()
|
||||||
|
.to_string()
|
||||||
|
.replace('/', "_")
|
||||||
|
.replace('.', "_dot_")
|
||||||
|
.replace('-', "_hyphen_")
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
if axum_route.is_empty() {
|
if axum_route.is_empty() {
|
||||||
return AxumInfo {
|
return AxumInfo {
|
||||||
module_import: module.as_str().to_string().replace('/', "_"),
|
module_import,
|
||||||
axum_route: "/".to_string(),
|
axum_route: "/".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_dynamic_path(&path) {
|
if route.is_dynamic {
|
||||||
|
let dyn_re = Regex::new(r"\[(.*?)\]").expect("Failed to create dyn regex");
|
||||||
|
let catch_all_re =
|
||||||
|
Regex::new(r"\{\.\.\.(.*?)\}").expect("Failed to create catch all regex");
|
||||||
|
|
||||||
|
let dyn_result = dyn_re.replace_all(&axum_route, |caps: ®ex::Captures| {
|
||||||
|
format!("{{{}}}", &caps[1])
|
||||||
|
});
|
||||||
|
|
||||||
|
let axum_route = catch_all_re
|
||||||
|
.replace_all(&dyn_result, |caps: ®ex::Captures| {
|
||||||
|
format!("{{*{}}}", &caps[1])
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
return AxumInfo {
|
return AxumInfo {
|
||||||
module_import: module
|
module_import: module
|
||||||
.as_str()
|
.as_str()
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace('/', "_")
|
.replace('/', "_")
|
||||||
|
.replace('-', "_hyphen_")
|
||||||
.replace('[', "dyn_")
|
.replace('[', "dyn_")
|
||||||
|
.replace("...", "catch_all_")
|
||||||
.replace(']', ""),
|
.replace(']', ""),
|
||||||
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
axum_route,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AxumInfo {
|
AxumInfo {
|
||||||
module_import: module.as_str().to_string().replace('/', "_").to_lowercase(),
|
module_import,
|
||||||
axum_route,
|
axum_route,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
// TODO: to be extended with common scenarios
|
||||||
|
const NO_HTML_EXTENSIONS: [&str; 2] = ["xml", "txt"];
|
||||||
|
|
||||||
|
// 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.to_uppercase().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 {
|
pub struct Route {
|
||||||
path: String,
|
path: String,
|
||||||
pub is_dynamic: bool,
|
pub is_dynamic: bool,
|
||||||
pub axum_info: Option<AxumInfo>,
|
pub axum_info: Option<AxumInfo>,
|
||||||
|
pub api_data: Option<ApiData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
@@ -66,73 +145,137 @@ impl Route {
|
|||||||
path: cleaned_path.clone(),
|
path: cleaned_path.clone(),
|
||||||
axum_info: None,
|
axum_info: None,
|
||||||
is_dynamic: has_dynamic_path(&cleaned_path),
|
is_dynamic: has_dynamic_path(&cleaned_path),
|
||||||
|
api_data: ApiData::new(&cleaned_path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_api(&self) -> bool {
|
||||||
|
self.api_data.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_axum_info(&mut self) {
|
pub fn update_axum_info(&mut self) {
|
||||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
self.axum_info = Some(AxumInfo::new(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_ssg_html(&self, reqwest: &Client) {
|
pub fn save_ssg_file(&self, reqwest: &Client) -> Result<(), String> {
|
||||||
let path = &self.path.replace("index", "");
|
if self.is_api() {
|
||||||
|
return Ok(());
|
||||||
let mut response = reqwest
|
|
||||||
.get(format!("http://localhost:3000{path}"))
|
|
||||||
.send()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let file_path = self.html_file_path();
|
|
||||||
|
|
||||||
let parent_dir = file_path.parent().unwrap();
|
|
||||||
|
|
||||||
if !parent_dir.is_dir() {
|
|
||||||
create_all(parent_dir, false).expect("Failed to create parent directories");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut file = File::create(file_path).expect("Failed to create the HTML file");
|
let path = &self.path.replace("index", "");
|
||||||
|
|
||||||
io::copy(&mut response, &mut file).expect("Failed to write the HTML on the file");
|
let url = format!("http://localhost:3000{path}");
|
||||||
|
|
||||||
|
trace!("Requesting the page: {}", url);
|
||||||
|
let mut response = match reqwest.get(format!("http://localhost:3000{path}")).send() {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(_) => return Err(format!("Failed to get the response: {url}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_path = self.output_file_path();
|
||||||
|
|
||||||
|
let parent_dir = match file_path.parent() {
|
||||||
|
Some(parent_dir) => parent_dir,
|
||||||
|
None => {
|
||||||
|
return Err(format!("Failed to get the parent directory {file_path:?}"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !parent_dir.is_dir() {
|
||||||
|
if let Err(err) = create_all(parent_dir, false) {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to create the parent directory {parent_dir:?}\nError: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("Saving the HTML file: {:?}", file_path);
|
||||||
|
|
||||||
|
let mut file = match File::create(&file_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return Err(format!("Failed to create the file: {file_path:?}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if io::copy(&mut response, &mut file).is_err() {
|
||||||
|
return Err(format!("Failed to write the file: {file_path:?}"));
|
||||||
|
}
|
||||||
|
|
||||||
// Saving also the server response
|
// Saving also the server response
|
||||||
if self.axum_info.is_some() {
|
if self.axum_info.is_some() {
|
||||||
let data_file_path =
|
trace!("The route is an axum route, saving the JSON file");
|
||||||
PathBuf::from(&format!("out/static/__tuono/data{}/data.json", path));
|
|
||||||
|
|
||||||
let data_parent_dir = data_file_path.parent().unwrap();
|
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}.json"));
|
||||||
|
|
||||||
|
let data_parent_dir = match data_file_path.parent() {
|
||||||
|
Some(parent_dir) => parent_dir,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to get the parent directory {data_file_path:?}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if !data_parent_dir.is_dir() {
|
if !data_parent_dir.is_dir() {
|
||||||
create_all(data_parent_dir, false)
|
if let Err(err) = create_all(data_parent_dir, false) {
|
||||||
.expect("Failed to create data parent directories");
|
return Err(format!(
|
||||||
|
"Failed to create the parent directory {data_parent_dir:?}\n Error: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 path = if path == "/" { "" } else { path };
|
||||||
|
|
||||||
let pathname = &format!("/__tuono/data{path}/data.json");
|
let pathname = &format!("/__tuono/data{path}");
|
||||||
|
|
||||||
let url = base
|
let url = base
|
||||||
.join(pathname)
|
.join(pathname)
|
||||||
.expect("Failed to build the reqwest URL");
|
.expect("Failed to build the reqwest URL");
|
||||||
|
|
||||||
let mut response = reqwest.get(url).send().unwrap();
|
trace!("Requesting the JSON file: {}", url);
|
||||||
|
|
||||||
let mut data_file =
|
let mut response = match reqwest.get(url.clone()).send() {
|
||||||
File::create(data_file_path).expect("Failed to create the JSON file");
|
Ok(response) => {
|
||||||
|
trace!("Successfully got the response for: {url}");
|
||||||
|
response
|
||||||
|
}
|
||||||
|
Err(_) => return Err(format!("Failed to get the response: {url}")),
|
||||||
|
};
|
||||||
|
|
||||||
io::copy(&mut response, &mut data_file).expect("Failed to write the JSON on the file");
|
let mut data_file = match File::create(&data_file_path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to create the JSON file: {data_file_path:?}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if io::copy(&mut response, &mut data_file).is_err() {
|
||||||
|
return Err("Failed to write the JSON file".to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn html_file_path(&self) -> PathBuf {
|
fn output_file_path(&self) -> PathBuf {
|
||||||
let cleaned_path = self.path.replace("index", "");
|
let cleaned_path = self.path.replace("index", "");
|
||||||
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
|
|
||||||
|
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{cleaned_path}/index.html"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -155,16 +298,28 @@ mod tests {
|
|||||||
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
|
.for_each(|route| assert_eq!(has_dynamic_path(route.0), route.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_allow_index_in_route_name() {
|
||||||
|
let index_route = AxumInfo::new(&Route::new("/index".to_string()));
|
||||||
|
let index_page_route = AxumInfo::new(&Route::new("/index-page".to_string()));
|
||||||
|
|
||||||
|
assert_eq!(index_route.axum_route, "/");
|
||||||
|
assert_eq!(index_route.module_import, "index");
|
||||||
|
|
||||||
|
assert_eq!(index_page_route.axum_route, "/index-page");
|
||||||
|
assert_eq!(index_page_route.module_import, "index_hyphen_page");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_correctly_create_the_axum_infos() {
|
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.axum_route, "/");
|
||||||
assert_eq!(info.module_import, "index");
|
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.axum_route, "/{posts}");
|
||||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +328,8 @@ mod tests {
|
|||||||
let routes = [
|
let routes = [
|
||||||
("/index", "out/static/index.html"),
|
("/index", "out/static/index.html"),
|
||||||
("/documentation", "out/static/documentation/index.html"),
|
("/documentation", "out/static/documentation/index.html"),
|
||||||
|
("/sitemap.xml", "out/static/sitemap.xml"),
|
||||||
|
("/robot.txt", "out/static/robot.txt"),
|
||||||
(
|
(
|
||||||
"/documentation/routing",
|
"/documentation/routing",
|
||||||
"out/static/documentation/routing/index.html",
|
"out/static/documentation/routing/index.html",
|
||||||
@@ -182,7 +339,7 @@ mod tests {
|
|||||||
for (path, html) in routes {
|
for (path, html) in routes {
|
||||||
let route = Route::new(path.to_string());
|
let route = Route::new(path.to_string());
|
||||||
|
|
||||||
assert_eq!(route.html_file_path(), PathBuf::from(html))
|
assert_eq!(route.output_file_path(), PathBuf::from(html))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
use clap::crate_version;
|
|
||||||
use reqwest::blocking;
|
|
||||||
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_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/Valerioageno/tuono/main/";
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
enum GithubFileType {
|
|
||||||
#[serde(rename = "blob")]
|
|
||||||
Blob,
|
|
||||||
#[serde(rename = "tree")]
|
|
||||||
Tree,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct GithubResponse<T> {
|
|
||||||
tree: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct GithubFile {
|
|
||||||
path: String,
|
|
||||||
#[serde(rename(deserialize = "type"))]
|
|
||||||
element_type: GithubFileType,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
|
|
||||||
let mut file = File::create(path)?;
|
|
||||||
let _ = file.write_all(content.as_bytes());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_new_project(folder_name: Option<String>, template: Option<String>) {
|
|
||||||
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 client = blocking::Client::builder()
|
|
||||||
.user_agent("")
|
|
||||||
.build()
|
|
||||||
.expect("Failed to build reqwest client");
|
|
||||||
|
|
||||||
let res = client
|
|
||||||
.get(GITHUB_TUONO_REPO_URL)
|
|
||||||
.send()
|
|
||||||
.expect("Failed to call the folder github API")
|
|
||||||
.json::<GithubResponse<GithubFile>>()
|
|
||||||
.expect("Failed to parse the repo structure");
|
|
||||||
|
|
||||||
let new_project_files = res
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if folder != "." {
|
|
||||||
create_dir(&folder).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_name = PathBuf::from(&folder);
|
|
||||||
let current_dir = env::current_dir().expect("Failed to get current working directory");
|
|
||||||
|
|
||||||
let folder_path = current_dir.join(folder_name);
|
|
||||||
|
|
||||||
create_directories(&new_project_files, &folder_path, &template)
|
|
||||||
.expect("Failed to create directories");
|
|
||||||
|
|
||||||
for GithubFile {
|
|
||||||
element_type, path, ..
|
|
||||||
} in new_project_files.iter()
|
|
||||||
{
|
|
||||||
if let GithubFileType::Blob = element_type {
|
|
||||||
let file_content = client
|
|
||||||
.get(format!("{GITHUB_RAW_CONTENT_URL}{path}"))
|
|
||||||
.send()
|
|
||||||
.expect("Failed to call the folder github API")
|
|
||||||
.text()
|
|
||||||
.expect("Failed to parse the repo structure");
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update_package_json_version(&folder_path).expect("Failed to update package.json version");
|
|
||||||
update_cargo_toml_version(&folder_path).expect("Failed to update Cargo.toml version");
|
|
||||||
outro(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_directories(
|
|
||||||
new_project_files: &[&GithubFile],
|
|
||||||
folder_path: &Path,
|
|
||||||
template: &String,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
for GithubFile {
|
|
||||||
element_type, path, ..
|
|
||||||
} in new_project_files.iter()
|
|
||||||
{
|
|
||||||
if let GithubFileType::Tree = element_type {
|
|
||||||
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
|
|
||||||
|
|
||||||
let dir_path = folder_path.join(&path);
|
|
||||||
create_dir(&dir_path).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 = package_json.replace("link:../../packages/tuono", v);
|
|
||||||
|
|
||||||
let mut file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(package_json_path)?;
|
|
||||||
|
|
||||||
file.write_all(package_json.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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 mut file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(cargo_toml_path)?;
|
|
||||||
|
|
||||||
file.write_all(cargo_toml.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn outro(folder_name: String) {
|
|
||||||
println!("Success! 🎉");
|
|
||||||
|
|
||||||
if folder_name != "." {
|
|
||||||
println!("\nGo to the project directory:");
|
|
||||||
println!("cd {folder_name}/");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\nInstall the dependencies:");
|
|
||||||
println!("npm install");
|
|
||||||
|
|
||||||
println!("\nRun the local environment:");
|
|
||||||
println!("tuono dev");
|
|
||||||
}
|
|
||||||
+270
-145
@@ -1,169 +1,253 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::crate_version;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::mode::Mode;
|
use crate::mode::Mode;
|
||||||
use crate::route::AxumInfo;
|
use crate::route::AxumInfo;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
|
use crate::typescript::TypesJar;
|
||||||
|
|
||||||
pub const SERVER_ENTRY_DATA: &str = "// File automatically generated by tuono
|
#[cfg(not(target_os = "windows"))]
|
||||||
// Do not manually update this file
|
const FALLBACK_HTML: &str = include_str!("../templates/fallback.html");
|
||||||
import { routeTree } from './routeTree.gen'
|
#[cfg(not(target_os = "windows"))]
|
||||||
import { serverSideRendering } from 'tuono/ssr'
|
const SERVER_ENTRY_DATA: &str = include_str!("../templates/server.ts");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
const CLIENT_ENTRY_DATA: &str = include_str!("../templates/client.ts");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
const AXUM_ENTRY_POINT: &str = include_str!("../templates/server.rs");
|
||||||
|
|
||||||
export const renderFn = serverSideRendering(routeTree)
|
#[cfg(not(target_os = "windows"))]
|
||||||
";
|
const MAIN_FILE_PATH: &str = "./.tuono/main.rs";
|
||||||
|
|
||||||
pub const CLIENT_ENTRY_DATA: &str = "// File automatically generated by tuono
|
#[cfg(not(target_os = "windows"))]
|
||||||
// Do not manually update this file
|
const FALLBACK_HTML_PATH: &str = "./.tuono/index.html";
|
||||||
import 'vite/modulepreload-polyfill'
|
|
||||||
import { hydrate } from 'tuono/hydration'
|
|
||||||
import '../src/styles/global.css'
|
|
||||||
|
|
||||||
// Import the generated route tree
|
|
||||||
import { routeTree } from './routeTree.gen'
|
|
||||||
|
|
||||||
hydrate(routeTree)
|
|
||||||
";
|
|
||||||
|
|
||||||
pub const AXUM_ENTRY_POINT: &str = r##"
|
|
||||||
// File automatically generated
|
|
||||||
// Do not manually change it
|
|
||||||
|
|
||||||
use tuono_lib::{tokio, Mode, Server, axum::Router};
|
|
||||||
// AXUM_GET_ROUTE_HANDLER
|
|
||||||
|
|
||||||
const MODE: Mode = /*MODE*/;
|
|
||||||
|
|
||||||
// MODULE_IMPORTS
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let router = Router::new()
|
|
||||||
// ROUTE_BUILDER
|
|
||||||
;
|
|
||||||
|
|
||||||
Server::init(router, MODE).start().await
|
|
||||||
}
|
|
||||||
"##;
|
|
||||||
|
|
||||||
const ROUTE_FOLDER: &str = "src/routes";
|
const ROUTE_FOLDER: &str = "src/routes";
|
||||||
const DEV_FOLDER: &str = ".tuono";
|
const DEV_FOLDER: &str = ".tuono";
|
||||||
|
|
||||||
fn create_main_file(base_path: &Path, bundled_file: &String) {
|
#[cfg(target_os = "windows")]
|
||||||
let mut data_file =
|
const FALLBACK_HTML: &str = include_str!("..\\templates\\fallback.html");
|
||||||
fs::File::create(base_path.join(".tuono/main.rs")).expect("creation failed");
|
#[cfg(target_os = "windows")]
|
||||||
|
const SERVER_ENTRY_DATA: &str = include_str!("..\\templates\\server.ts");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const CLIENT_ENTRY_DATA: &str = include_str!("..\\templates\\client.ts");
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const AXUM_ENTRY_POINT: &str = include_str!("..\\templates\\server.rs");
|
||||||
|
|
||||||
data_file
|
#[cfg(target_os = "windows")]
|
||||||
.write_all(bundled_file.as_bytes())
|
const MAIN_FILE_PATH: &str = ".\\.tuono\\main.rs";
|
||||||
.expect("write failed");
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const FALLBACK_HTML_PATH: &str = ".\\.tuono\\index.html";
|
||||||
|
|
||||||
|
// Use this function to instruct the users on how to
|
||||||
|
// fix their setup to make tuono work
|
||||||
|
fn recoverable_error(message: &str) -> ! {
|
||||||
|
error!("{}", message);
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
// Struct to build the source code
|
||||||
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
|
// on both "dev" and "build" commands
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SourceBuilder {
|
||||||
|
pub app: App,
|
||||||
|
mode: Mode,
|
||||||
|
base_path: PathBuf,
|
||||||
|
types_jar: TypesJar,
|
||||||
|
}
|
||||||
|
|
||||||
for (_, route) in routes.iter() {
|
impl SourceBuilder {
|
||||||
let Route { axum_info, .. } = &route;
|
pub fn new(mode: Mode) -> io::Result<Self> {
|
||||||
|
if !PathBuf::from("tuono.config.ts").exists() {
|
||||||
if axum_info.is_some() {
|
recoverable_error("Cannot find tuono.config.ts - is this a tuono project?");
|
||||||
let AxumInfo {
|
|
||||||
axum_route,
|
|
||||||
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))"#
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dev_folder = Path::new(DEV_FOLDER);
|
||||||
|
if !&dev_folder.is_dir() {
|
||||||
|
fs::create_dir(dev_folder)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = App::new();
|
||||||
|
|
||||||
|
let base_path = std::env::current_dir()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
app,
|
||||||
|
mode,
|
||||||
|
types_jar: TypesJar::from(&base_path),
|
||||||
|
base_path,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
route_declarations
|
// Build the source code needed for both build and dev
|
||||||
}
|
pub fn base_build(&mut self) -> io::Result<()> {
|
||||||
|
let mode = self.mode.clone();
|
||||||
|
|
||||||
fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
|
self.refresh_axum_source()?;
|
||||||
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
|
let dev_folder = Path::new(DEV_FOLDER);
|
||||||
|
self.create_file(dev_folder.join("server-main.tsx"), SERVER_ENTRY_DATA)?;
|
||||||
|
self.create_file(dev_folder.join("client-main.tsx"), CLIENT_ENTRY_DATA)?;
|
||||||
|
|
||||||
for (path, route) in routes.iter() {
|
self.types_jar.generate_typescript_file(&self.base_path)?;
|
||||||
if route.axum_info.is_some() {
|
|
||||||
let AxumInfo { module_import, .. } = route.axum_info.as_ref().unwrap();
|
|
||||||
|
|
||||||
route_declarations.push_str(&format!(
|
if mode == Mode::Dev {
|
||||||
r#"#[path="../{ROUTE_FOLDER}{path}.rs"]
|
self.app.build_tuono_config()?;
|
||||||
|
let fallback_html = self.build_html_fallback();
|
||||||
|
self.create_file(PathBuf::from(FALLBACK_HTML_PATH), &fallback_html)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_axum_source(&self) -> String {
|
||||||
|
let Self { app, mode, .. } = &self;
|
||||||
|
|
||||||
|
let src = AXUM_ENTRY_POINT
|
||||||
|
.replace("\r", "")
|
||||||
|
.replace("// ROUTE_BUILDER\n", &self.create_routes_declaration())
|
||||||
|
.replace("// MODULE_IMPORTS\n", &self.create_modules_declaration())
|
||||||
|
.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.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_axum_source(&self) -> io::Result<()> {
|
||||||
|
let axum_source = self.generate_axum_source();
|
||||||
|
|
||||||
|
self.create_file(PathBuf::from(MAIN_FILE_PATH), &axum_source)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_file(&self, path: PathBuf, content: &str) -> io::Result<()> {
|
||||||
|
let mut data_file = fs::File::create(self.base_path.join(path))?;
|
||||||
|
|
||||||
|
data_file.write_all(content.as_bytes())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_typescript_file(&mut self, path: PathBuf) {
|
||||||
|
self.types_jar.refresh_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_typescript_file(&mut self, path: PathBuf) {
|
||||||
|
self.types_jar.remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_typescript_file(&mut self) -> io::Result<()> {
|
||||||
|
self.types_jar.generate_typescript_file(&self.base_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_routes_declaration(&self) -> String {
|
||||||
|
let routes = &self.app.route_map;
|
||||||
|
let mut route_declarations = String::from("// ROUTE_BUILDER\n");
|
||||||
|
|
||||||
|
for (_, route) in routes.iter() {
|
||||||
|
let Route { axum_info, .. } = &route;
|
||||||
|
|
||||||
|
if axum_info.is_some() {
|
||||||
|
let AxumInfo {
|
||||||
|
axum_route,
|
||||||
|
module_import,
|
||||||
|
} = axum_info.as_ref().unwrap();
|
||||||
|
|
||||||
|
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))"#
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route_declarations
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_modules_declaration(&self) -> String {
|
||||||
|
let routes = &self.app.route_map;
|
||||||
|
let mut route_declarations = String::from("// MODULE_IMPORTS\n");
|
||||||
|
|
||||||
|
for (path, route) in routes.iter() {
|
||||||
|
if route.axum_info.is_some() {
|
||||||
|
let AxumInfo { module_import, .. } = route.axum_info.as_ref().unwrap();
|
||||||
|
|
||||||
|
route_declarations.push_str(&format!(
|
||||||
|
r#"#[path="../{ROUTE_FOLDER}{path}.rs"]
|
||||||
mod {module_import};
|
mod {module_import};
|
||||||
"#
|
"#
|
||||||
))
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route_declarations
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_html_fallback(&self) -> String {
|
||||||
|
if let Some(config) = &self.app.config.as_ref() {
|
||||||
|
if let Some(origin) = &config.server.origin {
|
||||||
|
FALLBACK_HTML.replace("[BASE_URL]", origin)
|
||||||
|
} else {
|
||||||
|
let url = format!("http://{}:{}", config.server.host, config.server.port);
|
||||||
|
FALLBACK_HTML.replace("[BASE_URL]", url.as_str())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route_declarations
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
|
||||||
let base_path = std::env::current_dir().unwrap();
|
|
||||||
|
|
||||||
let app = App::new();
|
|
||||||
|
|
||||||
let bundled_file = generate_axum_source(&app, mode);
|
|
||||||
|
|
||||||
create_main_file(&base_path, &bundled_file);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_axum_source(app: &App, mode: Mode) -> String {
|
|
||||||
let src = AXUM_ENTRY_POINT
|
|
||||||
.replace(
|
|
||||||
"// ROUTE_BUILDER\n",
|
|
||||||
&create_routes_declaration(&app.route_map),
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
"// 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;",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
src
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn check_tuono_folder() -> io::Result<()> {
|
|
||||||
let dev_folder = Path::new(DEV_FOLDER);
|
|
||||||
if !&dev_folder.is_dir() {
|
|
||||||
fs::create_dir(dev_folder)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_client_entry_files() -> io::Result<()> {
|
|
||||||
let dev_folder = Path::new(DEV_FOLDER);
|
|
||||||
|
|
||||||
let mut server_entry = fs::File::create(dev_folder.join("server-main.tsx"))?;
|
|
||||||
let mut client_entry = fs::File::create(dev_folder.join("client-main.tsx"))?;
|
|
||||||
|
|
||||||
server_entry.write_all(SERVER_ENTRY_DATA.as_bytes())?;
|
|
||||||
client_entry.write_all(CLIENT_ENTRY_DATA.as_bytes())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -173,36 +257,77 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_set_the_correct_mode() {
|
fn should_set_the_correct_mode() {
|
||||||
let source_builder = App::new();
|
let dev_bundle = SourceBuilder {
|
||||||
|
app: App::new(),
|
||||||
|
mode: Mode::Dev,
|
||||||
|
base_path: PathBuf::new(),
|
||||||
|
types_jar: TypesJar::default(),
|
||||||
|
}
|
||||||
|
.generate_axum_source();
|
||||||
|
|
||||||
|
let prod_bundle = SourceBuilder {
|
||||||
|
app: App::new(),
|
||||||
|
mode: Mode::Prod,
|
||||||
|
base_path: PathBuf::new(),
|
||||||
|
types_jar: TypesJar::default(),
|
||||||
|
}
|
||||||
|
.generate_axum_source();
|
||||||
|
|
||||||
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
|
||||||
assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;"));
|
assert!(dev_bundle.contains("const MODE: Mode = Mode::Dev;"));
|
||||||
|
|
||||||
let prod_bundle = generate_axum_source(&source_builder, Mode::Prod);
|
|
||||||
|
|
||||||
assert!(prod_bundle.contains("const MODE: Mode = Mode::Prod;"));
|
assert!(prod_bundle.contains("const MODE: Mode = Mode::Prod;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_not_load_the_axum_get_function() {
|
fn should_not_load_the_axum_get_function() {
|
||||||
let source_builder = App::new();
|
let dev_bundle = SourceBuilder {
|
||||||
|
app: App::new(),
|
||||||
|
mode: Mode::Dev,
|
||||||
|
base_path: PathBuf::new(),
|
||||||
|
types_jar: TypesJar::default(),
|
||||||
|
}
|
||||||
|
.generate_axum_source();
|
||||||
|
|
||||||
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
|
||||||
assert!(!dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
assert!(!dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_load_the_axum_get_function() {
|
fn should_load_the_axum_get_function() {
|
||||||
let mut source_builder = App::new();
|
let mut source_builder = SourceBuilder {
|
||||||
|
app: App::new(),
|
||||||
|
mode: Mode::Dev,
|
||||||
|
base_path: PathBuf::new(),
|
||||||
|
types_jar: TypesJar::default(),
|
||||||
|
};
|
||||||
|
|
||||||
let mut route = Route::new(String::from("index.tsx"));
|
let mut route = Route::new(String::from("index.tsx"));
|
||||||
route.update_axum_info();
|
route.update_axum_info();
|
||||||
|
|
||||||
source_builder
|
source_builder
|
||||||
|
.app
|
||||||
.route_map
|
.route_map
|
||||||
.insert(String::from("index.rs"), route);
|
.insert(String::from("index.rs"), route);
|
||||||
|
|
||||||
let dev_bundle = generate_axum_source(&source_builder, Mode::Dev);
|
let dev_bundle = source_builder.generate_axum_source();
|
||||||
|
|
||||||
assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
assert!(dev_bundle.contains("use tuono_lib::axum::routing::get;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_create_fallback_html_with_default_config() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.config = Some(Default::default());
|
||||||
|
|
||||||
|
let source_builder = SourceBuilder {
|
||||||
|
app,
|
||||||
|
mode: Mode::Dev,
|
||||||
|
base_path: PathBuf::new(),
|
||||||
|
types_jar: TypesJar::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let fallback_html = source_builder.build_html_fallback();
|
||||||
|
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/@react-refresh"));
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/@vite/client"));
|
||||||
|
assert!(fallback_html.contains("http://localhost:3000/vite-server/client-main.tsx"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use syn::Ident;
|
||||||
|
|
||||||
|
pub struct Symbol(&'static str);
|
||||||
|
|
||||||
|
// The trait name that allow automatic struct/enum/type conversion
|
||||||
|
// to typescript
|
||||||
|
pub const TYPE_TRAIT: Symbol = Symbol("Type");
|
||||||
|
|
||||||
|
impl PartialEq<Symbol> for Ident {
|
||||||
|
fn eq(&self, word: &Symbol) -> bool {
|
||||||
|
self == word.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<Symbol> for &Ident {
|
||||||
|
fn eq(&self, word: &Symbol) -> bool {
|
||||||
|
*self == word.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Symbol {
|
||||||
|
type Target = &'static str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use super::utils::has_derive_type;
|
||||||
|
use crate::typescript::parser::{parse_enum, parse_struct};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
/// Represents all the valid typescript types found in a file.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FileTypes {
|
||||||
|
/// Rust file when the type was found
|
||||||
|
pub file_path: PathBuf,
|
||||||
|
/// All the types found in the file
|
||||||
|
/// ready to be printed in the typescript file
|
||||||
|
pub types_as_string: String,
|
||||||
|
/// The types found in the file.
|
||||||
|
/// Used to check that the types are not duplicated across files.
|
||||||
|
pub types: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<(PathBuf, String)> for FileTypes {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
|
||||||
|
fn try_from((file_path, file_str): (PathBuf, String)) -> Result<Self, Self::Error> {
|
||||||
|
trace!("Parsing file: {:?}", &file_path);
|
||||||
|
let file = syn::parse_file(&file_str)?;
|
||||||
|
|
||||||
|
let mut types_as_string = String::new();
|
||||||
|
let mut types = Vec::new();
|
||||||
|
|
||||||
|
for item in file.items {
|
||||||
|
match item {
|
||||||
|
syn::Item::Struct(element) => {
|
||||||
|
if !has_derive_type(&element.attrs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trace!("Found struct in file: {:?}", &file_path);
|
||||||
|
let (struct_name, typescript_definition) = parse_struct(&element);
|
||||||
|
types_as_string.push_str(&typescript_definition);
|
||||||
|
types.push(struct_name);
|
||||||
|
}
|
||||||
|
syn::Item::Enum(element) => {
|
||||||
|
if !has_derive_type(&element.attrs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
trace!("Found enum in file: {:?}", &file_path);
|
||||||
|
|
||||||
|
let (enum_name, typescript_definition) = parse_enum(&element);
|
||||||
|
types_as_string.push_str(&typescript_definition);
|
||||||
|
types.push(enum_name);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if types_as_string.is_empty() {
|
||||||
|
return Err("No types found in the file".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
file_path,
|
||||||
|
types_as_string,
|
||||||
|
types,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_correctly_creates_type_from_pathbuf_and_string() {
|
||||||
|
let file_path = PathBuf::from("src/types.rs");
|
||||||
|
let file_str = r#"
|
||||||
|
#[derive(Type)]
|
||||||
|
struct MyStruct {
|
||||||
|
field1: String,
|
||||||
|
field2: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Type)]
|
||||||
|
enum MyEnum {
|
||||||
|
Variant1,
|
||||||
|
Variant2,
|
||||||
|
}
|
||||||
|
"#
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let ttype = FileTypes::try_from((file_path.clone(), file_str)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(ttype.file_path, file_path);
|
||||||
|
assert_eq!(
|
||||||
|
ttype.types_as_string,
|
||||||
|
"export interface MyStruct {\n field1: string;\n field2: number;\n}\nexport type MyEnum = \"Variant1\" | \"Variant2\";\n"
|
||||||
|
);
|
||||||
|
assert_eq!(ttype.types, vec!["MyStruct", "MyEnum"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user