mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-27 13:52:47 -07:00
Compare commits
443 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 2cbcd8ad70 | |||
| 9f40a55907 | |||
| 51e01ec142 | |||
| 63fc2b9855 | |||
| edd8a02423 | |||
| a0a81b514c | |||
| 92820804f8 | |||
| e7ea7ed1c4 | |||
| 7471c90f49 | |||
| f545a9c469 | |||
| 30073c226d | |||
| 1b1e91b6c0 | |||
| 71d7ccf27d | |||
| 3ca24ec87f | |||
| b2940d9fa8 | |||
| a7b7f0eac2 | |||
| 4859916edd | |||
| ad3e19e965 | |||
| 03dead1083 | |||
| 341fba6ca9 | |||
| 58c4890122 | |||
| af42e5ef21 | |||
| 5abff0384c | |||
| cc915560b8 |
@@ -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
|
||||
*.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,18 +1,24 @@
|
||||
## Context
|
||||
|
||||
<!--
|
||||
Explain the context and why you're making that change. What is the problem
|
||||
you're trying to solve? If a new feature is being added, describe the intended
|
||||
use case that feature fulfills.
|
||||
👋 Thank you for your Pull Request 🙏
|
||||
-->
|
||||
|
||||
## Description
|
||||
### Checklist
|
||||
|
||||
- [ ] I have read [Contributing > Pull requests](https://tuono.dev/documentation/contributing/pull-requests)
|
||||
|
||||
### Related issue
|
||||
|
||||
Fixes #0
|
||||
|
||||
<!-- Replace the content with "None" if you haven't an issue to link -->
|
||||
|
||||
### Overview
|
||||
|
||||
<!--
|
||||
Thank you for your Pull Request. Please provide a description above and review
|
||||
the requirements below.
|
||||
|
||||
Bug fixes and new features should include tests.
|
||||
Explain the context and why you're making that change.
|
||||
What is the problem you're trying to solve?
|
||||
If a new feature is being added,
|
||||
describe the intended use case that feature fulfills.
|
||||
|
||||
Contributors guide: https://github.com/Valerioageno/tuono/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
@@ -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,61 @@
|
||||
name: E2E CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
name: E2E Test (${{ matrix.os }})
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install
|
||||
|
||||
- name: Run Playwright E2E tests
|
||||
run: pnpm test:e2e
|
||||
|
||||
ci_ok:
|
||||
name: E2E CI OK
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
if: always()
|
||||
needs: [e2e]
|
||||
env:
|
||||
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
|
||||
steps:
|
||||
- name: Check for failure or cancelled jobs result
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
|
||||
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -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,78 +1,68 @@
|
||||
name: Publish on npm and crates
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-on-npm-dry-run:
|
||||
publish_npm_dry_run:
|
||||
name: NPM publish (dry run)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates-dry-run:
|
||||
publish_crates_dry_run:
|
||||
name: Crates publish (dry run)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
dry-run: true
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
publish-on-npm:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
publish_npm:
|
||||
name: NPM publish
|
||||
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Clone root README.md on tuono package
|
||||
run: cp README.md packages/tuono/README.md
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
@@ -81,17 +71,22 @@ jobs:
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
publish-on-crates:
|
||||
needs: [publish-on-crates-dry-run, publish-on-npm-dry-run]
|
||||
publish_crates:
|
||||
name: Crates publish
|
||||
needs: [publish_npm_dry_run, publish_crates_dry_run]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable'
|
||||
|
||||
- uses: katyo/publish-crates@v2
|
||||
with:
|
||||
path: './crates/tuono'
|
||||
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
check-repo: true
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Repo root CI
|
||||
|
||||
# All workflows are triggered based on specific paths,
|
||||
# the root repository is something that is not applicable to rust nor typescript
|
||||
# so it has been moved in a standalone workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: Check format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Test project
|
||||
run: pnpm repo:root:format
|
||||
|
||||
ci_ok:
|
||||
name: Repo root CI OK
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
if: always()
|
||||
needs: [format]
|
||||
env:
|
||||
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
|
||||
steps:
|
||||
- name: Check for failure or cancelled jobs result
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
|
||||
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,86 @@
|
||||
name: Rust CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Build and test crates on ${{ matrix.os }} with rust ${{ matrix.toolchain }}
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
toolchain:
|
||||
- 'stable'
|
||||
|
||||
# Test `beta` and `nightly` toolchains only on `ubuntu`
|
||||
# https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow#expanding-or-adding-matrix-configurations
|
||||
include:
|
||||
- toolchain: 'beta'
|
||||
os: 'ubuntu-latest'
|
||||
- toolchain: 'nightly'
|
||||
os: 'ubuntu-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
||||
- run: cargo build --verbose
|
||||
- run: cargo test --verbose
|
||||
|
||||
lint_and_fmt:
|
||||
name: Check format and lint rust crates
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
ci_ok:
|
||||
name: Rust CI OK
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
if: always()
|
||||
needs: [build_and_test, lint_and_fmt]
|
||||
env:
|
||||
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
|
||||
steps:
|
||||
- name: Check for failure or cancelled jobs result
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
|
||||
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -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,80 @@
|
||||
name: Typescript CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
name: Check build and test
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'macos-latest'
|
||||
- 'windows-latest'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Test project
|
||||
run: pnpm test
|
||||
|
||||
lint_and_fmt:
|
||||
name: Check format and lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install NodeJS Dependencies
|
||||
uses: ./.github/actions/install-node-dependencies
|
||||
|
||||
- name: Check formatting
|
||||
run: pnpm 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]
|
||||
env:
|
||||
FAILURE: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
CANCELLED: ${{ contains(join(needs.*.result, ','), 'cancelled') }}
|
||||
steps:
|
||||
- name: Check for failure or cancelled jobs result
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Failure: $FAILURE - Cancelled: $CANCELLED"
|
||||
if [ "$FAILURE" = "false" ] && [ "$CANCELLED" = "false" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -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/
|
||||
dist/
|
||||
.turbo/
|
||||
|
||||
*/**/routeTree.gen.ts
|
||||
|
||||
pnpm-lock.yaml
|
||||
examples/*/pnpm-lock.yaml
|
||||
|
||||
examples/playground/
|
||||
vite.config.ts.timestamp*
|
||||
|
||||
## Rust related ignores
|
||||
|
||||
@@ -24,3 +26,7 @@ examples/playground/
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# Files, generated by the code editors
|
||||
.vscode/
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
pnpm-lock.yaml
|
||||
|
||||
dist
|
||||
.tuono
|
||||
|
||||
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,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
* @Valerioageno @marcalexiei
|
||||
|
||||
# Rust
|
||||
/crates/ @Valerioageno
|
||||
/Cargo.toml @Valerioageno
|
||||
|
||||
# Misc
|
||||
/.github/ @marcalexiei
|
||||
|
||||
+15
-15
@@ -2,7 +2,7 @@
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
We, as members, contributors, and leaders, pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
- Focusing on what is best, not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
Advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
- Other conduct that could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -45,13 +45,13 @@ or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
not aligned with this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
An individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
|
||||
+16
-11
@@ -1,21 +1,26 @@
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
# How to contribute to Tuono
|
||||
# How to Contribute to Tuono
|
||||
|
||||
## Contributions
|
||||
|
||||
## Contributions
|
||||
Any feature contribution or suggestion is strongly appreciated.
|
||||
Since the current project size there isn't yet a defined way to start a discussion. Consider to [open a new issue](https://github.com/Valerioageno/tuono/issues/new)
|
||||
or to reach me using my email address [valerioageno@yahoo.it](mailto:valerioageno@ahoo.it). I'm also available on twitter (X) DMs @valerioageno.
|
||||
|
||||
We are currently managing the tasks' prioritization in the [Road to V1](https://github.com/orgs/tuono-labs/projects/2) project. You can pick any task that you see in the `ready` column that is not already assigned.
|
||||
|
||||
If you want to contribute on something that is in the `backlog` column, or it hasn't been scoped yet, please drop a message in the [#general](https://discord.com/invite/khQzPa654B) discord channel so we can all have visibility of it.
|
||||
|
||||
Consider [opening a new issue](https://github.com/tuono-labs/tuono/issues/new/choose) if you find a bug.
|
||||
|
||||
## Bugs
|
||||
|
||||
**Did you find a bug?**
|
||||
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Valerioageno/tuono/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Valerioageno/tuono/issues/new). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
|
||||
|
||||
- Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/tuono-labs/tuono/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/tuono-labs/tuono/issues/new/choose). Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring.
|
||||
|
||||
**Did you write a patch that fixes a bug?**
|
||||
- Open a new GitHub pull request with the patch.
|
||||
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
|
||||
- The pull requests must pass all the CI pipelines
|
||||
|
||||
[Visit the documentation to learn how to open a pull request](https://tuono.dev/documentation/contributing/pull-requests)
|
||||
|
||||
+9
-7
@@ -1,13 +1,15 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/tuono",
|
||||
"crates/tuono_lib",
|
||||
"crates/tuono_lib_macros",
|
||||
"crates/tuono",
|
||||
"crates/tuono_internal",
|
||||
"crates/tuono_lib",
|
||||
"crates/tuono_lib_macros"
|
||||
]
|
||||
exclude = [
|
||||
"apps/documentation",
|
||||
"examples/mdx",
|
||||
"examples/tuono",
|
||||
"examples/tutorial"
|
||||
"examples/with-mdx",
|
||||
"examples/tuono-app",
|
||||
"examples/tuono-tutorial",
|
||||
"examples/with-tailwind",
|
||||
"e2e/fixtures/base",
|
||||
]
|
||||
|
||||
@@ -1,98 +1,62 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Valerioageno/tuono/main/assets/logo.png" width="200px">
|
||||
</p>
|
||||
<h1 align="center">Tuono<br>The react/rust fullstack framework</h1>
|
||||
<div align="center">
|
||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/rust.yml/badge.svg" />
|
||||
<img src="https://github.com/Valerioageno/tuono/actions/workflows/typescript.yml/badge.svg" />
|
||||
</div>
|
||||
[Documentation](https://tuono.dev) | [Tutorial](https://tuono.dev/documentation/tutorial) |
|
||||
[Discord](https://discord.com/invite/khQzPa654B) | [✨Contributing](https://tuono.dev/documentation/contributing)
|
||||
|
||||
<br>
|
||||
<br>
|
||||
# Tuono
|
||||
|
||||
<img align="right" src="https://raw.githubusercontent.com/tuono-labs/tuono/main/assets/logo.png" width="160px">
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||

|
||||

|
||||
|
||||
**Tuono is a full-stack web framework for building React applications using Rust as the backend with
|
||||
a strong focus on usability and performance.**
|
||||
|
||||
Tuono (Italian word for "thunder", pronounced /2 Oh No/).
|
||||
Why Tuono? Just a badass name.
|
||||
|
||||
> Check out the [tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md) to get started.
|
||||
If you have experience with [Next.js](https://nextjs.org/) you will feel home here.
|
||||
|
||||
## Introduction
|
||||
Some of its features are:
|
||||
|
||||
**NodeJs/Deno/Bun are the only runtimes that allow a React app to be fullstack right? (no)**
|
||||
- 🟦 Native Typescript
|
||||
- 🌐 Next.js like Routing
|
||||
- 🍭 CSS/SCSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🔥 Hot Module Reload
|
||||
|
||||
Tuono is a fullstack React framework with the server side written in Rust.
|
||||
Because of this Tuono is extremely fast and the requests are handled by multithreaded Rust server.
|
||||
React is still React - it is just superpowered.
|
||||
## Documentation
|
||||
|
||||
**Rust is an hard language then writing server side code is hard as well right? (no again)**
|
||||
The [documentation](https://tuono.dev/) is available on
|
||||
[tuono.dev](https://tuono.dev/).
|
||||
|
||||
Tuono provides a collection of utilities to handle the server side code seamlessly with the React code.
|
||||
Each server side route is managed with a separate file alongside the React route. The routing is handled
|
||||
by Tuono based on the files defined within the `./src/routes` directory.
|
||||
## Installation
|
||||
|
||||
## Features
|
||||
Available on MacOS, Linux and Windows.
|
||||
|
||||
- 🟦 Typescript
|
||||
- 🌐 Routing
|
||||
- 🔥 Hot Module Reload
|
||||
- 🍭 CSS modules
|
||||
- 🧬 Server Side Rendering
|
||||
- 🧵 Multi thread backend
|
||||
- ⌨️ MDX support
|
||||
- ⚙️ Build optimizations
|
||||
- Custom APIs*
|
||||
- Image optimization*
|
||||
- Server streamed content*
|
||||
|
||||
> *Development in progress
|
||||
```sh
|
||||
cargo install tuono
|
||||
```
|
||||
|
||||
## Getting started
|
||||
|
||||
As already mentioned above I strongly suggest you to take a look at the
|
||||
[tutorial](https://github.com/Valerioageno/tuono/blob/main/docs/tutorial.md).
|
||||
|
||||
Tuono is the CLI that provides all the needed commands to handle the fullstack project.
|
||||
To download it is required [cargo](https://doc.rust-lang.org/cargo/) which is the [rust](https://www.rust-lang.org/)
|
||||
package manager.
|
||||
|
||||
Then run `cargo install tuono`.
|
||||
|
||||
To list all the available commands run `tuono -h`
|
||||
|
||||
To create a new project run `tuono new [NAME]` (optionally you can pass the `--template` (or `-t`) flag - check the
|
||||
[examples](https://github.com/Valerioageno/tuono/tree/main/examples) folder).
|
||||
|
||||
Then to run the local development environment run inside the project folder `tuono dev`
|
||||
|
||||
Finally when the project will be ready to be deployed just run `tuono build` to create the final React assets
|
||||
and to set the server project in the `production` mode.
|
||||
|
||||
Now to execute it just run `cargo run --release`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- rust
|
||||
- cargo
|
||||
- node
|
||||
- npm/pnpm/yarn*
|
||||
|
||||
> yarn [pnp](https://yarnpkg.com/features/pnp) is not supported yet
|
||||
|
||||
## Folder structure
|
||||
Create a new project with:
|
||||
|
||||
```sh
|
||||
tuono new [PROJECT_NAME]
|
||||
```
|
||||
├── package.json
|
||||
├── public
|
||||
├── src
|
||||
│ ├── routes
|
||||
│ └── styles
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
└── tsconfig.json
|
||||
|
||||
> You can optionally pass the --template (or -t) flag to directly start from a [template](https://github.com/tuono-labs/tuono/tree/main/examples)
|
||||
|
||||
Now just install the dependencies with you favourite JS package manager (i.e. `npm install`) and start the development environment
|
||||
|
||||
```sh
|
||||
tuono dev
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Any help or suggestion will be appreciated and encouraged.
|
||||
Check the [✨Contributing](https://tuono.dev/documentation/contributing) page
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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.8.3"
|
||||
},
|
||||
"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,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/installation" 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,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,8 +0,0 @@
|
||||
import { Title } from '@mantine/core'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
|
||||
export default function MdxH2(
|
||||
props: HTMLAttributes<HTMLHeadingElement>,
|
||||
): JSX.Element {
|
||||
return <Title {...props} mt={20} order={2} />
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import MdxLink from './mdx-link'
|
||||
|
||||
export default MdxLink
|
||||
@@ -1,37 +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')) {
|
||||
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"
|
||||
rightSection={<IconExternalLink size="16px" style={{ marginLeft: -5 }} />}
|
||||
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,33 +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 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,
|
||||
}}
|
||||
>
|
||||
{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"
|
||||
p={8}
|
||||
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,8 +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} />
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Flex, Button, ActionIcon } 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/installation"
|
||||
component={Link}
|
||||
size="compact-lg"
|
||||
rightSection={<IconBook />}
|
||||
autoContrast
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<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 />
|
||||
</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">
|
||||
<Flex align="center" gap={8}>
|
||||
{pathname.startsWith('/documentation') && (
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
<Button component={Link} href="/" variant="transparent" p={0} fz={28}>
|
||||
Tuono
|
||||
</Button>
|
||||
</Flex>
|
||||
<Actions />
|
||||
</Flex>
|
||||
</AppShell.Header>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import Sidebar from './sidebar'
|
||||
|
||||
export default Sidebar
|
||||
@@ -1,33 +0,0 @@
|
||||
import { AppShell, NavLink } from '@mantine/core'
|
||||
import { Link } from 'tuono'
|
||||
|
||||
export default function Sidebar(): JSX.Element {
|
||||
return (
|
||||
<AppShell.Navbar p="md">
|
||||
<h3>Tutorial</h3>
|
||||
<NavLink
|
||||
href="/documentation/tutorial/intro"
|
||||
component={Link}
|
||||
label="Intro"
|
||||
/>
|
||||
<h3>Documentation</h3>
|
||||
<NavLink
|
||||
href="/documentation/installation"
|
||||
component={Link}
|
||||
label="Installation"
|
||||
/>
|
||||
<NavLink label="Routing" href="#required-for-focus" defaultOpened>
|
||||
<NavLink
|
||||
href="/documentation/routing/intro"
|
||||
component={Link}
|
||||
label="FS routing"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
label="Contributing"
|
||||
component={Link}
|
||||
href="/documentation/contributing"
|
||||
/>
|
||||
</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,75 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
ColorSchemeScript,
|
||||
createTheme,
|
||||
MantineProvider,
|
||||
AppShell,
|
||||
} from '@mantine/core'
|
||||
import { useDisclosure } from '@mantine/hooks'
|
||||
import { Head } from 'tuono'
|
||||
import Navbar from '../components/navbar'
|
||||
|
||||
import '@mantine/core/styles.css'
|
||||
import '@mantine/code-highlight/styles.css'
|
||||
|
||||
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()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<ColorSchemeScript />
|
||||
</Head>
|
||||
<MantineProvider theme={theme}>
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
>
|
||||
<Navbar opened={opened} toggle={toggle} />
|
||||
{children}
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { AppShell, Container } from '@mantine/core'
|
||||
import MdxProvider from '../../components/mdx-provider'
|
||||
import Sidebar from '../../components/sidebar'
|
||||
import EditPage from '../../components/edit-page'
|
||||
|
||||
interface RootRouteProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootRoute({ children }: RootRouteProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Sidebar />
|
||||
<AppShell.Main>
|
||||
<Container component="article" p={20}>
|
||||
<MdxProvider>{children}</MdxProvider>
|
||||
<EditPage />
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Contributing</title>
|
||||
</Head>
|
||||
|
||||
# How to contribute
|
||||
|
||||
## TL;DR
|
||||
|
||||
The project is massive - if you like it do consider to contribute!
|
||||
|
||||
## Getting started
|
||||
|
||||
Currently I'm keeping a private dashboard to prioritize new features and bug fixes.
|
||||
|
||||
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
|
||||
|
||||
To contribute is not strictly required to know both typescript and rust (even though it
|
||||
would be a great nice to have). Most of the feature tasks and bugfixes can be handled solely on
|
||||
the client or on the server side.
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Installation</title>
|
||||
</Head>
|
||||
|
||||
# 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,9 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Routing</title>
|
||||
</Head>
|
||||
|
||||
# Routing
|
||||
|
||||
Todo
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Head } from 'tuono'
|
||||
|
||||
<Head>
|
||||
<title>Tuono - Tutorial intro</title>
|
||||
</Head>
|
||||
|
||||
# Tutorial intro
|
||||
|
||||
TODO
|
||||
@@ -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"]
|
||||
}
|
||||
+22
-12
@@ -1,18 +1,16 @@
|
||||
[package]
|
||||
name = "tuono"
|
||||
version = "0.8.4"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = ["react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
include = [
|
||||
"src/*.rs",
|
||||
"Cargo.toml"
|
||||
]
|
||||
include = ["src/*.rs", "Cargo.toml"]
|
||||
|
||||
[lib]
|
||||
name = "tuono"
|
||||
@@ -20,15 +18,27 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive", "cargo"] }
|
||||
watchexec = "4.0.0"
|
||||
watchexec = "5.0.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = {version = "0.3.19", features = ["env-filter"]}
|
||||
miette = "7.2.0"
|
||||
watchexec-signals = "3.0.0"
|
||||
watchexec-signals = "4.0.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
watchexec-supervisor = "2.0.0"
|
||||
watchexec-supervisor = "3.0.0"
|
||||
glob = "0.3.1"
|
||||
regex = "1.10.4"
|
||||
reqwest = {version = "0.12.4", features =["blocking", "json"]}
|
||||
reqwest = { version = "0.12.4", features = ["blocking", "json"] }
|
||||
serde_json = "1.0"
|
||||
fs_extra = "1.3.0"
|
||||
http = "1.1.0"
|
||||
tuono_internal = {path = "../tuono_internal", version = "0.17.9"}
|
||||
spinners = "4.1.1"
|
||||
console = "0.15.10"
|
||||
|
||||
[dev-dependencies]
|
||||
wiremock = "0.6.2"
|
||||
tempfile = "3.14.0"
|
||||
assert_cmd = "2.0.16"
|
||||
serial_test = "0.10.0"
|
||||
|
||||
|
||||
+247
-30
@@ -1,29 +1,67 @@
|
||||
use crate::mode::Mode;
|
||||
use glob::glob;
|
||||
use glob::GlobError;
|
||||
use http::Method;
|
||||
use std::collections::hash_set::HashSet;
|
||||
use std::collections::{hash_map::Entry, HashMap};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use tracing::error;
|
||||
use tuono_internal::config::Config;
|
||||
|
||||
use crate::route::Route;
|
||||
|
||||
const IGNORE_EXTENSIONS: [&str; 3] = ["css", "scss", "sass"];
|
||||
const IGNORE_FILES: [&str; 1] = ["__root"];
|
||||
const IGNORE_FILES: [&str; 1] = ["__layout"];
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg(target_os = "windows")]
|
||||
const ROUTES_FOLDER_PATH: &str = "\\src\\routes";
|
||||
#[cfg(target_os = "windows")]
|
||||
const BUILD_JS_SCRIPT: &str = ".\\node_modules\\.bin\\tuono-build-prod.cmd";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.cmd";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const ROUTES_FOLDER_PATH: &str = "/src/routes";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const BUILD_JS_SCRIPT: &str = "./node_modules/.bin/tuono-build-prod";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const BUILD_TUONO_CONFIG: &str = "./node_modules/.bin/tuono-build-config";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct App {
|
||||
pub route_map: HashMap<String, Route>,
|
||||
pub base_path: PathBuf,
|
||||
pub has_app_state: bool,
|
||||
pub config: Option<Config>,
|
||||
}
|
||||
|
||||
fn has_app_state(base_path: PathBuf) -> std::io::Result<bool> {
|
||||
let file = File::open(base_path.join("src/app.rs"))?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = String::new();
|
||||
buf_reader.read_to_string(&mut contents)?;
|
||||
Ok(contents.contains("pub fn main"))
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let base_path = std::env::current_dir().unwrap();
|
||||
let base_path = std::env::current_dir().expect("Failed to read current_dir");
|
||||
|
||||
let mut app = App {
|
||||
route_map: HashMap::new(),
|
||||
base_path,
|
||||
base_path: base_path.clone(),
|
||||
has_app_state: has_app_state(base_path).unwrap_or(false),
|
||||
config: None,
|
||||
};
|
||||
|
||||
app.collect_routes();
|
||||
@@ -32,18 +70,31 @@ impl App {
|
||||
}
|
||||
|
||||
fn collect_routes(&mut self) {
|
||||
glob(self.base_path.join("src/routes/**/*.*").to_str().unwrap())
|
||||
.expect("Failed to read glob pattern")
|
||||
.for_each(|entry| {
|
||||
if self.should_collect_route(&entry) {
|
||||
self.collect_route(entry)
|
||||
}
|
||||
})
|
||||
glob(
|
||||
self.base_path
|
||||
.join("src/routes/**/*.*")
|
||||
.to_str()
|
||||
.expect("Failed to glob routes folder"),
|
||||
)
|
||||
.expect("Failed to read glob pattern")
|
||||
.for_each(|entry| {
|
||||
if self.should_collect_route(&entry) {
|
||||
self.collect_route(entry)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn should_collect_route(&self, entry: &Result<PathBuf, GlobError>) -> bool {
|
||||
let file_extension = entry.as_ref().unwrap().extension().unwrap();
|
||||
let file_name = entry.as_ref().unwrap().file_stem().unwrap();
|
||||
let file_extension = entry
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.extension()
|
||||
.expect("Failed to read file extension");
|
||||
let file_name = entry
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.file_stem()
|
||||
.expect("Failed to read file name");
|
||||
|
||||
if IGNORE_EXTENSIONS.iter().any(|val| val == &file_extension) {
|
||||
return false;
|
||||
@@ -56,17 +107,23 @@ impl App {
|
||||
}
|
||||
|
||||
fn collect_route(&mut self, path_buf: Result<PathBuf, GlobError>) {
|
||||
let entry = path_buf.unwrap();
|
||||
let base_path_str = self.base_path.to_str().unwrap();
|
||||
let entry = path_buf.expect("Failed to read glob path");
|
||||
|
||||
let base_path_str = self
|
||||
.base_path
|
||||
.to_str()
|
||||
.expect("Failed to read as str base_path");
|
||||
let path = entry
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace(&format!("{base_path_str}/src/routes"), "")
|
||||
.expect("Failed to read entry as str")
|
||||
.replace(&format!("{base_path_str}{ROUTES_FOLDER_PATH}"), "")
|
||||
// Cleanup windows paths
|
||||
.replace("\\", "/")
|
||||
.replace(".rs", "")
|
||||
.replace(".mdx", "")
|
||||
.replace(".tsx", "");
|
||||
|
||||
if entry.extension().unwrap() == "rs" {
|
||||
if entry.extension().expect("failed to read entry extension") == "rs" {
|
||||
if let Entry::Vacant(route_map) = self.route_map.entry(path.clone()) {
|
||||
let mut route = Route::new(path);
|
||||
route.update_axum_info();
|
||||
@@ -87,10 +144,54 @@ impl App {
|
||||
self.route_map.iter().any(|(_, route)| route.is_dynamic)
|
||||
}
|
||||
|
||||
pub fn check_server_availability(&self, mode: Mode) {
|
||||
// At this point the config should be available
|
||||
let config = self.config.as_ref().unwrap();
|
||||
|
||||
let rust_listener =
|
||||
std::net::TcpListener::bind(format!("{}:{}", config.server.host, config.server.port));
|
||||
|
||||
if let Err(_e) = rust_listener {
|
||||
eprintln!("Error: Failed to bind to port {}", config.server.port);
|
||||
eprintln!(
|
||||
"Please ensure that port {} is not already in use by another process or application.",
|
||||
config.server.port
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if mode == Mode::Dev {
|
||||
let vite_port = config.server.port + 1;
|
||||
let vite_listener =
|
||||
std::net::TcpListener::bind(format!("{}:{}", config.server.host, vite_port));
|
||||
|
||||
if let Err(_e) = vite_listener {
|
||||
eprintln!("Error: Failed to bind to port {}", vite_port);
|
||||
eprintln!(
|
||||
"Please ensure that port {} is not already in use by another process or application.",
|
||||
vite_port
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_react_prod(&self) {
|
||||
Command::new("./node_modules/.bin/tuono-build-prod")
|
||||
.output()
|
||||
.expect("Failed to build the react source");
|
||||
if !Path::new(BUILD_JS_SCRIPT).exists() {
|
||||
error!("Failed to find the build script. Please run `npm install`");
|
||||
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 {
|
||||
@@ -102,6 +203,52 @@ impl App {
|
||||
.spawn()
|
||||
.expect("Failed to run the rust server")
|
||||
}
|
||||
|
||||
pub fn build_tuono_config(&mut self) -> Result<std::process::Output, std::io::Error> {
|
||||
if !Path::new(BUILD_TUONO_CONFIG).exists() {
|
||||
eprintln!("Failed to find the build script. Please run `npm install`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let config_build_command = Command::new(BUILD_TUONO_CONFIG)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output();
|
||||
|
||||
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)]
|
||||
@@ -112,14 +259,36 @@ mod tests {
|
||||
#[test]
|
||||
fn should_collect_routes() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\handle-this\\[post].rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\UPPERCASE.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\sitemap.xml.rs",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/index.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/[post].rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/handle-this.rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/handle-this/[post].rs",
|
||||
"/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs",
|
||||
"/home/user/Documents/tuono/src/routes/sitemap.xml.rs",
|
||||
];
|
||||
|
||||
routes
|
||||
@@ -131,7 +300,13 @@ mod tests {
|
||||
("/about", "about"),
|
||||
("/posts/index", "posts_index"),
|
||||
("/posts/[post]", "posts_dyn_post"),
|
||||
("/posts/handle-this", "posts_handle_hyphen_this"),
|
||||
(
|
||||
"/posts/handle-this/[post]",
|
||||
"posts_handle_hyphen_this_dyn_post",
|
||||
),
|
||||
("/posts/UPPERCASE", "posts_uppercase"),
|
||||
("/sitemap.xml", "sitemap_dot_xml"),
|
||||
];
|
||||
|
||||
results.into_iter().for_each(|(path, module_import)| {
|
||||
@@ -151,8 +326,25 @@ mod tests {
|
||||
#[test]
|
||||
fn should_create_multi_level_axum_paths() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\index.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\any-post.rs",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\posts\\[post].rs",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/index.rs",
|
||||
@@ -170,14 +362,14 @@ mod tests {
|
||||
("/about", "/about"),
|
||||
("/posts/index", "/posts"),
|
||||
("/posts/any-post", "/posts/any-post"),
|
||||
("/posts/[post]", "/posts/:post"),
|
||||
("/posts/[post]", "/posts/{post}"),
|
||||
];
|
||||
|
||||
results.into_iter().for_each(|(path, expected_path)| {
|
||||
assert_eq!(
|
||||
app.route_map
|
||||
.get(path)
|
||||
.unwrap()
|
||||
.expect("Failed to get route path")
|
||||
.axum_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -213,8 +405,8 @@ mod tests {
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/__root.tsx",
|
||||
"/home/user/Documents/tuono/src/routes/posts/__root.tsx",
|
||||
"/home/user/Documents/tuono/src/routes/__layout.tsx",
|
||||
"/home/user/Documents/tuono/src/routes/posts/__layout.tsx",
|
||||
];
|
||||
|
||||
routes.into_iter().for_each(|route| {
|
||||
@@ -229,8 +421,23 @@ mod tests {
|
||||
#[test]
|
||||
fn should_correctly_parse_routes_with_server_handler() {
|
||||
let mut app = App::new();
|
||||
app.base_path = "/home/user/Documents/tuono".into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let base_path = "\\home\\user\\Documents\\tuono";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let base_path = "/home/user/Documents/tuono";
|
||||
|
||||
app.base_path = base_path.into();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let routes = [
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\about.rs",
|
||||
"\\home\\user\\Documents/tuono\\src\\routes\\about.tsx",
|
||||
"\\home\\user\\Documents\\tuono\\src\\routes\\index.tsx",
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let routes = [
|
||||
"/home/user/Documents/tuono/src/routes/about.rs",
|
||||
"/home/user/Documents/tuono/src/routes/about.tsx",
|
||||
@@ -247,9 +454,19 @@ mod tests {
|
||||
.into_iter()
|
||||
.for_each(|(path, expected_has_server_handler)| {
|
||||
if expected_has_server_handler {
|
||||
assert!(app.route_map.get(path).unwrap().axum_info.is_some())
|
||||
assert!(app
|
||||
.route_map
|
||||
.get(path)
|
||||
.expect("Failed to get route path")
|
||||
.axum_info
|
||||
.is_some())
|
||||
} else {
|
||||
assert!(app.route_map.get(path).unwrap().axum_info.is_none())
|
||||
assert!(app
|
||||
.route_map
|
||||
.get(path)
|
||||
.expect("Failed to get route path")
|
||||
.axum_info
|
||||
.is_none())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
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 = 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());
|
||||
}
|
||||
}
|
||||
+50
-64
@@ -1,11 +1,11 @@
|
||||
use fs_extra::dir::{copy, CopyOptions};
|
||||
use std::path::PathBuf;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing::{span, Level};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::build;
|
||||
use crate::mode::Mode;
|
||||
use crate::scaffold_project;
|
||||
use crate::source_builder::{bundle_axum_source, check_tuono_folder, create_client_entry_files};
|
||||
@@ -20,6 +20,10 @@ enum Actions {
|
||||
#[arg(short, long = "static")]
|
||||
/// Statically generate the website HTML
|
||||
ssg: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
/// Prevent to export the js assets
|
||||
no_js_emit: bool,
|
||||
},
|
||||
/// Scaffold a new project
|
||||
New {
|
||||
@@ -29,96 +33,78 @@ enum Actions {
|
||||
/// examples
|
||||
#[arg(short, long)]
|
||||
template: Option<String>,
|
||||
/// Load the latest commit available on the main branch
|
||||
#[arg(long)]
|
||||
head: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "The react/rust fullstack framework")]
|
||||
#[command(version, about = "The React/Rust full-stack framework")]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
action: Actions,
|
||||
}
|
||||
|
||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<()> {
|
||||
fn check_for_tuono_config() {
|
||||
if !PathBuf::from("tuono.config.ts").exists() {
|
||||
eprintln!("Cannot find tuono.config.ts - is this a tuono project?");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tuono_folder(mode: Mode) -> std::io::Result<App> {
|
||||
check_for_tuono_config();
|
||||
check_tuono_folder()?;
|
||||
bundle_axum_source(mode)?;
|
||||
let app = bundle_axum_source(mode)?;
|
||||
create_client_entry_files()?;
|
||||
|
||||
Ok(())
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
pub fn app() -> std::io::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
// Not need for the time since the code is synchronous
|
||||
.without_time()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
match args.action {
|
||||
Actions::Dev => {
|
||||
init_tuono_folder(Mode::Dev)?;
|
||||
let span = span!(Level::TRACE, "DEV");
|
||||
|
||||
let _guard = span.enter();
|
||||
|
||||
let mut app = init_tuono_folder(Mode::Dev)?;
|
||||
|
||||
app.build_tuono_config()
|
||||
.expect("Failed to build tuono.config.ts");
|
||||
|
||||
app.check_server_availability(Mode::Dev);
|
||||
|
||||
watch::watch().unwrap();
|
||||
}
|
||||
Actions::Build { ssg } => {
|
||||
init_tuono_folder(Mode::Prod)?;
|
||||
let app = App::new();
|
||||
Actions::Build { ssg, no_js_emit } => {
|
||||
let span = span!(Level::TRACE, "BUILD");
|
||||
|
||||
if ssg && app.has_dynamic_routes() {
|
||||
// TODO: allow dynamic routes static generation
|
||||
println!("Cannot statically build dynamic routes");
|
||||
return Ok(());
|
||||
}
|
||||
let _guard = span.enter();
|
||||
|
||||
app.build_react_prod();
|
||||
let app = init_tuono_folder(Mode::Prod)?;
|
||||
|
||||
if ssg {
|
||||
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");
|
||||
build::build(app, ssg, no_js_emit);
|
||||
}
|
||||
Actions::New {
|
||||
folder_name,
|
||||
template,
|
||||
head,
|
||||
} => {
|
||||
scaffold_project::create_new_project(folder_name, template);
|
||||
let span = span!(Level::TRACE, "NEW");
|
||||
|
||||
let _guard = span.enter();
|
||||
|
||||
scaffold_project::create_new_project(folder_name, template, head);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
//! ## 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 build;
|
||||
pub mod cli;
|
||||
mod mode;
|
||||
mod route;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[derive(PartialEq, Eq)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub enum Mode {
|
||||
Prod,
|
||||
Dev,
|
||||
|
||||
+188
-43
@@ -1,17 +1,20 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use http::Method;
|
||||
use regex::Regex;
|
||||
use reqwest::blocking::Client;
|
||||
use reqwest::Url;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use tracing::trace;
|
||||
|
||||
fn has_dynamic_path(route: &str) -> bool {
|
||||
let regex = Regex::new(r"\[(.*?)\]").expect("Failed to create the regex");
|
||||
regex.is_match(route)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct AxumInfo {
|
||||
// Path for importing the module
|
||||
pub module_import: String,
|
||||
@@ -20,44 +23,116 @@ pub struct AxumInfo {
|
||||
}
|
||||
|
||||
impl AxumInfo {
|
||||
pub fn new(path: String) -> Self {
|
||||
pub fn new(route: &Route) -> Self {
|
||||
// Remove first slash
|
||||
let mut module = path.chars();
|
||||
let mut module = route.path.chars();
|
||||
module.next();
|
||||
|
||||
let axum_route = path.replace("/index", "");
|
||||
let axum_route = route.path.replace("/index", "");
|
||||
|
||||
let module_import = module
|
||||
.as_str()
|
||||
.to_string()
|
||||
.replace('/', "_")
|
||||
.replace('.', "_dot_")
|
||||
.replace('-', "_hyphen_")
|
||||
.to_lowercase();
|
||||
|
||||
if axum_route.is_empty() {
|
||||
return AxumInfo {
|
||||
module_import: module.as_str().to_string().replace('/', "_"),
|
||||
module_import,
|
||||
axum_route: "/".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if has_dynamic_path(&path) {
|
||||
if route.is_dynamic {
|
||||
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 {
|
||||
module_import: module
|
||||
.as_str()
|
||||
.to_string()
|
||||
.replace('/', "_")
|
||||
.replace('-', "_hyphen_")
|
||||
.replace('[', "dyn_")
|
||||
.replace("...", "catch_all_")
|
||||
.replace(']', ""),
|
||||
axum_route: axum_route.replace('[', ":").replace(']', ""),
|
||||
axum_route,
|
||||
};
|
||||
}
|
||||
|
||||
AxumInfo {
|
||||
module_import: module.as_str().to_string().replace('/', "_").to_lowercase(),
|
||||
module_import,
|
||||
axum_route,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
// TODO: to be extended with common scenarios
|
||||
const NO_HTML_EXTENSIONS: [&str; 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.as_str()).unwrap_or(Method::GET)
|
||||
})
|
||||
.collect::<Vec<Method>>()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ApiData {
|
||||
pub methods: Vec<Method>,
|
||||
}
|
||||
|
||||
impl ApiData {
|
||||
pub fn new(path: &String) -> Option<Self> {
|
||||
if !path.starts_with("/api/") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_path = std::env::current_dir().expect("Failed to get the base_path");
|
||||
|
||||
let file_path = base_path
|
||||
.join(format!("src/routes{path}.rs"))
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let methods = read_http_methods_from_file(&file_path);
|
||||
|
||||
Some(ApiData { methods })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Route {
|
||||
path: String,
|
||||
pub is_dynamic: bool,
|
||||
pub axum_info: Option<AxumInfo>,
|
||||
pub api_data: Option<ApiData>,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
@@ -66,66 +141,134 @@ impl Route {
|
||||
path: cleaned_path.clone(),
|
||||
axum_info: None,
|
||||
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) {
|
||||
self.axum_info = Some(AxumInfo::new(self.path.clone()))
|
||||
self.axum_info = Some(AxumInfo::new(self))
|
||||
}
|
||||
|
||||
pub fn save_ssg_html(&self, reqwest: &Client) {
|
||||
let path = &self.path.replace("index", "");
|
||||
|
||||
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");
|
||||
pub fn save_ssg_file(&self, reqwest: &Client) -> Result<(), String> {
|
||||
if self.is_api() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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() || create_all(parent_dir, false).is_err() {
|
||||
return Err(format!(
|
||||
"Failed to create the parent directory {:?}",
|
||||
parent_dir
|
||||
));
|
||||
}
|
||||
|
||||
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
|
||||
if self.axum_info.is_some() {
|
||||
let data_file_path =
|
||||
PathBuf::from(&format!("out/static/__tuono/data{}/data.json", path));
|
||||
trace!("The route is an axum route, saving the JSON file");
|
||||
|
||||
let data_parent_dir = data_file_path.parent().unwrap();
|
||||
let data_file_path = PathBuf::from(&format!("out/static/__tuono/data{path}.json"));
|
||||
|
||||
if !data_parent_dir.is_dir() {
|
||||
create_all(data_parent_dir, false)
|
||||
.expect("Failed to create data parent directories");
|
||||
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() && create_all(data_parent_dir, false).is_err() {
|
||||
return Err(format!(
|
||||
"Failed to create the parent directory {:?}",
|
||||
data_parent_dir
|
||||
));
|
||||
}
|
||||
|
||||
let base = Url::parse("http://localhost:3000/__tuono/data/data.json").unwrap();
|
||||
let base = Url::parse("http://localhost:3000/__tuono/data").unwrap();
|
||||
|
||||
let path = if path == "/" { "" } else { path };
|
||||
|
||||
let pathname = &format!("/__tuono/data{path}/data.json");
|
||||
let pathname = &format!("/__tuono/data{path}");
|
||||
|
||||
let url = base
|
||||
.join(pathname)
|
||||
.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 =
|
||||
File::create(data_file_path).expect("Failed to create the JSON file");
|
||||
let mut response = match reqwest.get(url.clone()).send() {
|
||||
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", "");
|
||||
|
||||
if NO_HTML_EXTENSIONS
|
||||
.iter()
|
||||
.any(|extension| self.path.ends_with(extension))
|
||||
{
|
||||
return PathBuf::from(format!("out/static{}", cleaned_path));
|
||||
}
|
||||
|
||||
PathBuf::from(format!("out/static{}/index.html", cleaned_path))
|
||||
}
|
||||
}
|
||||
@@ -157,14 +300,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn should_correctly_create_the_axum_infos() {
|
||||
let info = AxumInfo::new("/index".to_string());
|
||||
let info = AxumInfo::new(&Route::new("/index".to_string()));
|
||||
|
||||
assert_eq!(info.axum_route, "/");
|
||||
assert_eq!(info.module_import, "index");
|
||||
|
||||
let dyn_info = AxumInfo::new("/[posts]".to_string());
|
||||
let dyn_info = AxumInfo::new(&Route::new("/[posts]".to_string()));
|
||||
|
||||
assert_eq!(dyn_info.axum_route, "/:posts");
|
||||
assert_eq!(dyn_info.axum_route, "/{posts}");
|
||||
assert_eq!(dyn_info.module_import, "dyn_posts");
|
||||
}
|
||||
|
||||
@@ -173,6 +316,8 @@ mod tests {
|
||||
let routes = [
|
||||
("/index", "out/static/index.html"),
|
||||
("/documentation", "out/static/documentation/index.html"),
|
||||
("/sitemap.xml", "out/static/sitemap.xml"),
|
||||
("/robot.txt", "out/static/robot.txt"),
|
||||
(
|
||||
"/documentation/routing",
|
||||
"out/static/documentation/routing/index.html",
|
||||
@@ -182,7 +327,7 @@ mod tests {
|
||||
for (path, html) in routes {
|
||||
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,16 +1,12 @@
|
||||
use clap::crate_version;
|
||||
use reqwest::blocking;
|
||||
use reqwest::blocking::Client;
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
use std::fs::{self, create_dir, File, OpenOptions};
|
||||
use std::io::{self, prelude::*};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const GITHUB_TUONO_REPO_URL: &str =
|
||||
"https://api.github.com/repos/Valerioageno/tuono/git/trees/main?recursive=1";
|
||||
|
||||
const GITHUB_RAW_CONTENT_URL: &str = "https://raw.githubusercontent.com/Valerioageno/tuono/main/";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
enum GithubFileType {
|
||||
#[serde(rename = "blob")]
|
||||
@@ -20,10 +16,25 @@ enum GithubFileType {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GithubResponse<T> {
|
||||
struct GithubTagObject {
|
||||
sha: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GithubTagResponse {
|
||||
object: GithubTagObject,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GithubTreeResponse<T> {
|
||||
tree: Vec<T>,
|
||||
}
|
||||
|
||||
fn exit_with_error(message: &str) -> ! {
|
||||
eprintln!("{}", message);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct GithubFile {
|
||||
path: String,
|
||||
@@ -32,42 +43,73 @@ struct GithubFile {
|
||||
}
|
||||
|
||||
fn create_file(path: PathBuf, content: String) -> std::io::Result<()> {
|
||||
let mut file = File::create(path)?;
|
||||
let mut file = File::create(&path).unwrap_or_else(|err| {
|
||||
exit_with_error(&format!(
|
||||
"Failed to create file {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
));
|
||||
});
|
||||
let _ = file.write_all(content.as_bytes());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_new_project(folder_name: Option<String>, template: Option<String>) {
|
||||
pub fn create_new_project(
|
||||
folder_name: Option<String>,
|
||||
template: Option<String>,
|
||||
select_head: Option<bool>,
|
||||
) {
|
||||
let folder = folder_name.unwrap_or(".".to_string());
|
||||
|
||||
// In case of missing select the tuono example
|
||||
let template = template.unwrap_or("tuono".to_string());
|
||||
let 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()
|
||||
.expect("Failed to build reqwest client");
|
||||
.unwrap_or_else(|_| exit_with_error("Error: Failed to build request client"));
|
||||
|
||||
let res = client
|
||||
.get(GITHUB_TUONO_REPO_URL)
|
||||
// This string does not include the "v" version prefix
|
||||
let cli_version: &str = crate_version!();
|
||||
|
||||
let tree_url: String =
|
||||
generate_tree_url(select_head, &client, cli_version, &github_api_base_url);
|
||||
|
||||
let res_tree = client
|
||||
.get(tree_url)
|
||||
.send()
|
||||
.expect("Failed to call the folder github API")
|
||||
.json::<GithubResponse<GithubFile>>()
|
||||
.expect("Failed to parse the repo structure");
|
||||
.unwrap_or_else(|_| {
|
||||
exit_with_error(&format!(
|
||||
"Failed to call the tagged commit tree github API for v{cli_version}"
|
||||
))
|
||||
})
|
||||
.json::<GithubTreeResponse<GithubFile>>()
|
||||
.expect("Failed to parse the tree structure");
|
||||
|
||||
let new_project_files = res
|
||||
let new_project_files = res_tree
|
||||
.tree
|
||||
.iter()
|
||||
.filter(|GithubFile { path, .. }| path.starts_with(&format!("examples/{template}/")))
|
||||
.collect::<Vec<&GithubFile>>();
|
||||
|
||||
if new_project_files.is_empty() {
|
||||
println!("Template not found: {template}");
|
||||
return;
|
||||
eprintln!("Error: Template '{template}' not found");
|
||||
println!("Hint: you can view the available templates at https://github.com/tuono-labs/tuono/tree/main/examples");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if folder != "." {
|
||||
if Path::new(&folder).exists() {
|
||||
eprintln!("Error: Directory '{folder}' already exists");
|
||||
println!("Hint: you can scaffold a tuono project within an existing folder with 'cd {folder} && tuono new .'");
|
||||
std::process::exit(1);
|
||||
}
|
||||
create_dir(&folder).unwrap();
|
||||
}
|
||||
|
||||
@@ -77,25 +119,34 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
|
||||
let folder_path = current_dir.join(folder_name);
|
||||
|
||||
create_directories(&new_project_files, &folder_path, &template)
|
||||
.expect("Failed to create directories");
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to create directories: {}", err)));
|
||||
|
||||
for GithubFile {
|
||||
element_type, path, ..
|
||||
} in new_project_files.iter()
|
||||
{
|
||||
if let GithubFileType::Blob = element_type {
|
||||
let url =
|
||||
generate_raw_content_url(select_head, cli_version, path, &github_raw_base_url);
|
||||
|
||||
let file_content = client
|
||||
.get(format!("{GITHUB_RAW_CONTENT_URL}{path}"))
|
||||
.get(url)
|
||||
.send()
|
||||
.expect("Failed to call the folder github API")
|
||||
.text()
|
||||
.expect("Failed to parse the repo structure");
|
||||
.map_err(|_| exit_with_error("Failed to call the folder github API"))
|
||||
.and_then(|response| {
|
||||
response
|
||||
.text()
|
||||
.map_err(|_| exit_with_error("Failed to parse the repo structure"))
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
|
||||
|
||||
let file_path = folder_path.join(&path);
|
||||
|
||||
create_file(file_path, file_content).expect("failed to create file");
|
||||
if let Err(err) = create_file(file_path, file_content) {
|
||||
exit_with_error(&format!("Failed to create file: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +155,48 @@ pub fn create_new_project(folder_name: Option<String>, template: Option<String>)
|
||||
outro(folder);
|
||||
}
|
||||
|
||||
fn generate_raw_content_url(
|
||||
select_head: Option<bool>,
|
||||
cli_version: &str,
|
||||
path: &String,
|
||||
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,
|
||||
@@ -117,24 +210,31 @@ fn create_directories(
|
||||
let path = PathBuf::from(&path.replace(&format!("examples/{template}/"), ""));
|
||||
|
||||
let dir_path = folder_path.join(&path);
|
||||
create_dir(&dir_path).unwrap();
|
||||
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)?;
|
||||
let package_json = fs::read_to_string(&package_json_path)
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to read package.json: {}", err)));
|
||||
let package_json = package_json.replace("link:../../packages/tuono", v);
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(package_json_path)?;
|
||||
.open(package_json_path)
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open package.json: {}", err)));
|
||||
|
||||
file.write_all(package_json.as_bytes())?;
|
||||
file.write_all(package_json.as_bytes())
|
||||
.unwrap_or_else(|err| {
|
||||
exit_with_error(&format!("Failed to write to package.json: {}", err))
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -142,16 +242,21 @@ fn update_package_json_version(folder_path: &Path) -> io::Result<()> {
|
||||
fn update_cargo_toml_version(folder_path: &Path) -> io::Result<()> {
|
||||
let v = crate_version!();
|
||||
let cargo_toml_path = folder_path.join(PathBuf::from("Cargo.toml"));
|
||||
let cargo_toml = fs::read_to_string(&cargo_toml_path)?;
|
||||
let cargo_toml =
|
||||
cargo_toml.replace("{ path = \"../../crates/tuono_lib/\"}", &format!("\"{v}\""));
|
||||
let cargo_toml = fs::read_to_string(&cargo_toml_path)
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to read Cargo.toml: {}", err)));
|
||||
let cargo_toml = cargo_toml.replace(
|
||||
"{ path = \"../../crates/tuono_lib/\" }",
|
||||
&format!("\"{v}\""),
|
||||
);
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(cargo_toml_path)?;
|
||||
.open(cargo_toml_path)
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to open Cargo.toml: {}", err)));
|
||||
|
||||
file.write_all(cargo_toml.as_bytes())?;
|
||||
file.write_all(cargo_toml.as_bytes())
|
||||
.unwrap_or_else(|err| exit_with_error(&format!("Failed to write to Cargo.toml: {}", err)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -170,3 +275,39 @@ fn outro(folder_name: String) {
|
||||
println!("\nRun the local environment:");
|
||||
println!("tuono dev");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn generate_valid_content_url_from_head() {
|
||||
let expected = format!(
|
||||
"{}/{}/{}",
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
|
||||
use clap::crate_version;
|
||||
|
||||
use crate::app::App;
|
||||
use crate::mode::Mode;
|
||||
use crate::route::AxumInfo;
|
||||
@@ -33,20 +35,27 @@ pub const AXUM_ENTRY_POINT: &str = r##"
|
||||
// File automatically generated
|
||||
// Do not manually change it
|
||||
|
||||
use tuono_lib::{tokio, Mode, Server, axum::Router};
|
||||
use tuono_lib::{tokio, Mode, Server, axum::Router, tuono_internal_init_v8_platform};
|
||||
// AXUM_GET_ROUTE_HANDLER
|
||||
|
||||
const MODE: Mode = /*MODE*/;
|
||||
|
||||
// MODULE_IMPORTS
|
||||
|
||||
//MAIN_FILE_IMPORT//
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tuono_internal_init_v8_platform();
|
||||
println!("\n ⚡ Tuono v/*VERSION*/");
|
||||
|
||||
//MAIN_FILE_DEFINITION//
|
||||
|
||||
let router = Router::new()
|
||||
// ROUTE_BUILDER
|
||||
;
|
||||
//MAIN_FILE_USAGE//;
|
||||
|
||||
Server::init(router, MODE).start().await
|
||||
Server::init(router, MODE).await.start().await
|
||||
}
|
||||
"##;
|
||||
|
||||
@@ -74,13 +83,22 @@ fn create_routes_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
module_import,
|
||||
} = axum_info.as_ref().unwrap();
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::route))"#
|
||||
));
|
||||
let slash = if axum_route.ends_with('/') { "" } else { "/" };
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}{slash}data.json", get({module_import}::api))"#
|
||||
));
|
||||
if !route.is_api() {
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", get({module_import}::tuono_internal_route))"#
|
||||
));
|
||||
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("/__tuono/data{axum_route}", get({module_import}::tuono_internal_api))"#
|
||||
));
|
||||
} else {
|
||||
for method in route.api_data.as_ref().unwrap().methods.clone() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
route_declarations.push_str(&format!(
|
||||
r#".route("{axum_route}", {method}({module_import}::{method}_tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,16 +123,14 @@ fn create_modules_declaration(routes: &HashMap<String, Route>) -> String {
|
||||
route_declarations
|
||||
}
|
||||
|
||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<()> {
|
||||
pub fn bundle_axum_source(mode: Mode) -> io::Result<App> {
|
||||
let base_path = std::env::current_dir().unwrap();
|
||||
|
||||
let app = App::new();
|
||||
|
||||
let bundled_file = generate_axum_source(&app, mode);
|
||||
|
||||
create_main_file(&base_path, &bundled_file);
|
||||
|
||||
Ok(())
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||
@@ -127,22 +143,45 @@ fn generate_axum_source(app: &App, mode: Mode) -> String {
|
||||
"// MODULE_IMPORTS\n",
|
||||
&create_modules_declaration(&app.route_map),
|
||||
)
|
||||
.replace("/*MODE*/", mode.as_str());
|
||||
|
||||
let has_server_handlers = app
|
||||
.route_map
|
||||
.iter()
|
||||
.filter(|(_, route)| route.axum_info.is_some())
|
||||
.collect::<Vec<(&String, &Route)>>()
|
||||
.is_empty();
|
||||
|
||||
if !has_server_handlers {
|
||||
return src.replace(
|
||||
"// AXUM_GET_ROUTE_HANDLER",
|
||||
"use tuono_lib::axum::routing::get;",
|
||||
.replace("/*VERSION*/", crate_version!())
|
||||
.replace("/*MODE*/", mode.as_str())
|
||||
.replace(
|
||||
"//MAIN_FILE_IMPORT//",
|
||||
if app.has_app_state {
|
||||
r#"#[path="../src/app.rs"]
|
||||
mod tuono_main_state;
|
||||
"#
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"//MAIN_FILE_DEFINITION//",
|
||||
if app.has_app_state {
|
||||
"let user_custom_state = tuono_main_state::main();"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
"//MAIN_FILE_USAGE//",
|
||||
if app.has_app_state {
|
||||
".with_state(user_custom_state)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
);
|
||||
|
||||
let mut import_http_handler = String::new();
|
||||
|
||||
let used_http_methods = app.get_used_http_methods();
|
||||
|
||||
for method in used_http_methods.into_iter() {
|
||||
let method = method.to_string().to_lowercase();
|
||||
import_http_handler.push_str(&format!("use tuono_lib::axum::routing::{method};\n"))
|
||||
}
|
||||
src
|
||||
|
||||
src.replace("// AXUM_GET_ROUTE_HANDLER", &import_http_handler)
|
||||
}
|
||||
|
||||
pub fn check_tuono_folder() -> io::Result<()> {
|
||||
|
||||
+54
-12
@@ -1,3 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use watchexec_supervisor::command::{Command, Program};
|
||||
|
||||
@@ -8,11 +9,27 @@ use watchexec_supervisor::job::{start_job, Job};
|
||||
|
||||
use crate::mode::Mode;
|
||||
use crate::source_builder::bundle_axum_source;
|
||||
use console::Term;
|
||||
use spinners::{Spinner, Spinners};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const DEV_WATCH_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-watch.cmd";
|
||||
#[cfg(target_os = "windows")]
|
||||
const DEV_SSR_BIN_SRC: &str = "node_modules\\.bin\\tuono-dev-ssr.cmd";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const DEV_WATCH_BIN_SRC: &str = "node_modules/.bin/tuono-dev-watch";
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const DEV_SSR_BIN_SRC: &str = "node_modules/.bin/tuono-dev-ssr";
|
||||
|
||||
fn watch_react_src() -> Job {
|
||||
if !Path::new(DEV_SSR_BIN_SRC).exists() {
|
||||
eprintln!("Failed to find script to run dev watch. Please run `npm install`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-watch".into(),
|
||||
prog: DEV_WATCH_BIN_SRC.into(),
|
||||
args: vec![],
|
||||
},
|
||||
options: Default::default(),
|
||||
@@ -20,7 +37,7 @@ fn watch_react_src() -> Job {
|
||||
.0
|
||||
}
|
||||
|
||||
fn build_rust_src() -> Job {
|
||||
fn run_rust_dev_server() -> Job {
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "cargo".into(),
|
||||
@@ -31,10 +48,25 @@ fn build_rust_src() -> Job {
|
||||
.0
|
||||
}
|
||||
|
||||
fn build_react_ssr_src() -> Job {
|
||||
fn build_rust_src() -> Job {
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: "node_modules/.bin/tuono-dev-ssr".into(),
|
||||
prog: "cargo".into(),
|
||||
args: vec!["build".to_string(), "-q".to_string()],
|
||||
},
|
||||
options: Default::default(),
|
||||
}))
|
||||
.0
|
||||
}
|
||||
|
||||
fn build_react_ssr_src() -> Job {
|
||||
if !Path::new(DEV_SSR_BIN_SRC).exists() {
|
||||
eprintln!("Failed to find script to run dev ssr. Please run `npm install`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
start_job(Arc::new(Command {
|
||||
program: Program::Exec {
|
||||
prog: DEV_SSR_BIN_SRC.into(),
|
||||
args: vec![],
|
||||
},
|
||||
options: Default::default(),
|
||||
@@ -44,18 +76,28 @@ fn build_react_ssr_src() -> Job {
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn watch() -> Result<()> {
|
||||
println!("Starting development environment...");
|
||||
let term = Term::stdout();
|
||||
let mut sp = Spinner::new(Spinners::Dots, "Starting dev server...".into());
|
||||
|
||||
watch_react_src().start().await;
|
||||
|
||||
let run_server = build_rust_src();
|
||||
let rust_server = run_rust_dev_server();
|
||||
let build_rust_src = build_rust_src();
|
||||
|
||||
let build_ssr_bundle = build_react_ssr_src();
|
||||
|
||||
build_ssr_bundle.start().await;
|
||||
build_rust_src.start().await;
|
||||
|
||||
run_server.start().await;
|
||||
|
||||
// Wait vite and rust builds
|
||||
build_ssr_bundle.to_wait().await;
|
||||
build_rust_src.to_wait().await;
|
||||
|
||||
rust_server.start().await;
|
||||
|
||||
// Remove the spinner
|
||||
sp.stop();
|
||||
term.clear_line().unwrap();
|
||||
|
||||
let wx = Watchexec::new(move |mut action| {
|
||||
let mut should_reload_ssr_bundle = false;
|
||||
@@ -76,10 +118,10 @@ pub async fn watch() -> Result<()> {
|
||||
}
|
||||
|
||||
if should_reload_rust_server {
|
||||
println!("Reloading...");
|
||||
run_server.stop();
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bunlde rust source");
|
||||
run_server.start();
|
||||
println!(" Reloading...");
|
||||
rust_server.stop();
|
||||
bundle_axum_source(Mode::Dev).expect("Failed to bundle rust source");
|
||||
rust_server.start();
|
||||
}
|
||||
|
||||
if should_reload_ssr_bundle {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
use assert_cmd::Command;
|
||||
use serial_test::serial;
|
||||
use std::fs;
|
||||
|
||||
mod utils;
|
||||
use utils::temp_tuono_project::TempTuonoProject;
|
||||
|
||||
const POST_API_FILE: &str = r"#[tuono_lib::api(POST)]";
|
||||
const GET_API_FILE: &str = r"#[tuono_lib::api(GET)]";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const BUILD_TUONO_CONFIG: &str = ".\\node_modules\\.bin\\tuono-build-config.cmd";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const BUILD_TUONO_CONFIG: &str = "./node_modules/.bin/tuono-build-config";
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_the_index_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
|
||||
temp_tuono_project.add_file("./src/routes/index.rs");
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/index.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod index;"));
|
||||
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/", get(index::tuono_internal_route)).route("/__tuono/data/", get(index::tuono_internal_api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_an_api_route() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
|
||||
temp_tuono_project.add_file_with_content("./src/routes/api/health_check.rs", POST_API_FILE);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post_tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_multiple_api_for_the_same_file() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
|
||||
temp_tuono_project.add_file_with_content(
|
||||
"./src/routes/api/health_check.rs",
|
||||
&format!("{POST_API_FILE}{GET_API_FILE}"),
|
||||
);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/health_check.rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_health_check;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/health_check", post(api_health_check::post_tuono_internal_api))"#
|
||||
));
|
||||
assert!(temp_main_rs_content
|
||||
.contains(r#".route("/api/health_check", get(api_health_check::get_tuono_internal_api))"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_successfully_create_catch_all_routes() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
|
||||
temp_tuono_project.add_file("./src/routes/[...all_routes].rs");
|
||||
|
||||
temp_tuono_project.add_file_with_content("./src/routes/api/[...all_apis].rs", POST_API_FILE);
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.arg("--no-js-emit")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let temp_main_rs_path = temp_tuono_project.path().join(".tuono/main.rs");
|
||||
|
||||
let temp_main_rs_content =
|
||||
fs::read_to_string(&temp_main_rs_path).expect("Failed to read '.tuono/main.rs' content.");
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/api/[...all_apis].rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod api_dyn_catch_all_all_apis;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(r#"#[path="../src/routes/[...all_routes].rs"]"#));
|
||||
assert!(temp_main_rs_content.contains("mod dyn_catch_all_all_routes;"));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/api/{*all_apis}", post(api_dyn_catch_all_all_apis::post_tuono_internal_api))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_route))"#
|
||||
));
|
||||
|
||||
assert!(temp_main_rs_content.contains(
|
||||
r#".route("/__tuono/data/{*all_routes}", get(dyn_catch_all_all_routes::tuono_internal_api))"#
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_fails_without_installed_build_config_script() {
|
||||
let _guard = TempTuonoProject::new();
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr("Failed to find the build script. Please run `npm install`\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn it_fails_without_installed_build_script() {
|
||||
let temp_tuono_project = TempTuonoProject::new();
|
||||
temp_tuono_project.add_file_with_content(BUILD_TUONO_CONFIG, "#!/bin/bash");
|
||||
Command::new("chmod")
|
||||
.arg("+x")
|
||||
.arg(BUILD_TUONO_CONFIG)
|
||||
.assert()
|
||||
.success();
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
|
||||
test_tuono_build
|
||||
.arg("build")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr("Failed to read config. Please run `npm install` to generate automatically.\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn dev_fails_with_no_config() {
|
||||
let _temp_tuono_project = TempTuonoProject::new_with_no_config();
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("dev")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr("Cannot find tuono.config.ts - is this a tuono project?\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn build_fails_with_no_config() {
|
||||
let _temp_tuono_project = TempTuonoProject::new_with_no_config();
|
||||
|
||||
let mut test_tuono_build = Command::cargo_bin("tuono").unwrap();
|
||||
test_tuono_build
|
||||
.arg("dev")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr("Cannot find tuono.config.ts - is this a tuono project?\n");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use assert_cmd::Command;
|
||||
use clap::crate_version;
|
||||
use serial_test::serial;
|
||||
|
||||
use std::fs;
|
||||
|
||||
mod utils;
|
||||
|
||||
use utils::mock_github_endpoint::GitHubServerMock;
|
||||
use utils::temp_tuono_project::TempTuonoProject;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn it_creates_a_new_project_and_replace_the_versions_in_the_manifest() {
|
||||
let GitHubServerMock { env_vars, .. } = GitHubServerMock::new().await;
|
||||
|
||||
let temp_project = TempTuonoProject::new();
|
||||
|
||||
let project_folder = "my-project";
|
||||
|
||||
let mut cmd = Command::cargo_bin("tuono").unwrap();
|
||||
|
||||
cmd.arg("new")
|
||||
.arg(project_folder)
|
||||
.envs(env_vars)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let main_rs_path = temp_project
|
||||
.path()
|
||||
.join(project_folder)
|
||||
.join("src")
|
||||
.join("main.rs");
|
||||
|
||||
let cargo_toml_path = temp_project.path().join(project_folder).join("Cargo.toml");
|
||||
let package_json_path = temp_project
|
||||
.path()
|
||||
.join(project_folder)
|
||||
.join("package.json");
|
||||
|
||||
let main_rs_content = fs::read_to_string(main_rs_path).unwrap();
|
||||
let cargo_toml_content = fs::read_to_string(cargo_toml_path).unwrap();
|
||||
let package_json_content = fs::read_to_string(package_json_path).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
main_rs_content,
|
||||
"fn main() { println!(\"Hello, world!\"); }"
|
||||
);
|
||||
|
||||
let version = crate_version!();
|
||||
|
||||
let expected_cargo_toml_content =
|
||||
format!("[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = \"{version}\"");
|
||||
|
||||
assert_eq!(cargo_toml_content, expected_cargo_toml_content);
|
||||
|
||||
let expected_package_json_content =
|
||||
format!("{{\"name\": \"tuono-app\", \"dependencies\": {{ \"tuono\": \"{version}\" }}}}");
|
||||
|
||||
assert_eq!(package_json_content, expected_package_json_content);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use clap::crate_version;
|
||||
use std::env;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GitHubServerMock {
|
||||
pub server: MockServer,
|
||||
pub env_vars: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl GitHubServerMock {
|
||||
pub async fn new() -> Self {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let tuono_version = crate_version!();
|
||||
|
||||
let sha = "1234567890abcdef";
|
||||
|
||||
let sha_response_template = ResponseTemplate::new(200).set_body_raw(
|
||||
format!("{{ \"object\": {{ \"sha\": \"{sha}\" }} }}"),
|
||||
"application/json",
|
||||
);
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(&format!(
|
||||
"repos/tuono-labs/tuono/git/ref/tags/v{}",
|
||||
tuono_version
|
||||
)))
|
||||
.respond_with(sha_response_template)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let tree_response_template = ResponseTemplate::new(200).set_body_raw(
|
||||
r#"{
|
||||
"tree": [
|
||||
{
|
||||
"path": "examples/tuono-app/src",
|
||||
"type": "tree"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/src/main.rs",
|
||||
"type": "blob"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/Cargo.toml",
|
||||
"type": "blob"
|
||||
},
|
||||
{
|
||||
"path": "examples/tuono-app/package.json",
|
||||
"type": "blob"
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
"application/json",
|
||||
);
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(&format!("repos/tuono-labs/tuono/git/trees/{}", sha)))
|
||||
.respond_with(tree_response_template)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let file_response_template =
|
||||
|content: &str| ResponseTemplate::new(200).set_body_raw(content, "text/plain");
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/src/main.rs"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
"fn main() { println!(\"Hello, world!\"); }",
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/Cargo.toml"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
"[package] name = \"tuono-tutorial\" [dependencies] tuono_lib = { path = \"../../crates/tuono_lib/\" }"
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!(
|
||||
"tuono-labs/tuono/v{tuono_version}/examples/tuono-app/package.json"
|
||||
)))
|
||||
.respond_with(file_response_template(
|
||||
r#"{"name": "tuono-app", "dependencies": { "tuono": "link:../../packages/tuono" }}"#,
|
||||
))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let env_vars = vec![("__INTERNAL_TUONO_TEST".to_string(), server.uri())];
|
||||
GitHubServerMock { server, env_vars }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod mock_github_endpoint;
|
||||
pub mod temp_tuono_project;
|
||||
@@ -0,0 +1,70 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TempTuonoProject {
|
||||
original_dir: PathBuf,
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl TempTuonoProject {
|
||||
pub fn new() -> Self {
|
||||
let project = TempTuonoProject::new_with_no_config();
|
||||
|
||||
project.add_file("./tuono.config.ts");
|
||||
|
||||
project
|
||||
}
|
||||
|
||||
pub fn new_with_no_config() -> Self {
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
TempTuonoProject {
|
||||
original_dir,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.temp_dir.path()
|
||||
}
|
||||
|
||||
pub fn add_file(&self, path: &str) -> File {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("Route path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create parent route directory");
|
||||
File::create(path).expect("Failed to create the route file")
|
||||
}
|
||||
|
||||
pub fn add_file_with_content<'a>(&self, path: &'a str, content: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("Route path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create parent route directory");
|
||||
|
||||
let mut file = File::create(path).expect("Failed to create the route file");
|
||||
file.write_all(content.as_bytes())
|
||||
.expect("Failed to write into API file");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempTuonoProject {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(&self.original_dir)
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "tuono_internal"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
include = [
|
||||
"src/*.rs",
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.137"
|
||||
|
||||
[dev-dependencies]
|
||||
fs_extra = "1.3.0"
|
||||
tempfile = "3.14.0"
|
||||
assert_cmd = "2.0.16"
|
||||
serial_test = "0.10.0"
|
||||
@@ -0,0 +1,4 @@
|
||||
# tuono_internal
|
||||
|
||||
This crate is a collection of internal utilities for the Tuono project. It is
|
||||
not intended for use outside of the project.
|
||||
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::read_to_string;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn get() -> io::Result<Config> {
|
||||
let config_file = read_to_string(PathBuf::from_iter([".tuono", "config", "config.json"]))?;
|
||||
|
||||
serde_json::from_str(&config_file)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod config;
|
||||
@@ -0,0 +1,50 @@
|
||||
mod utils;
|
||||
use serial_test::serial;
|
||||
use std::io::ErrorKind;
|
||||
use tuono_internal::config::Config;
|
||||
use utils::TempTuonoProject;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_correctly_read_the_config_file() {
|
||||
let folder = TempTuonoProject::new();
|
||||
|
||||
folder.add_file_with_content(
|
||||
"./.tuono/config/config.json",
|
||||
r#"{ "server": {"host": "localhost", "port": 3000}}"#,
|
||||
);
|
||||
|
||||
let config = Config::get();
|
||||
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert_eq!(config.server.host, "localhost");
|
||||
assert_eq!(config.server.port, 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_fail_if_the_file_does_not_exist() {
|
||||
TempTuonoProject::new();
|
||||
|
||||
let config = Config::get();
|
||||
|
||||
assert!(config.is_err());
|
||||
|
||||
assert_eq!(config.err().unwrap().kind(), ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn should_fail_if_the_file_is_not_json() {
|
||||
let folder = TempTuonoProject::new();
|
||||
|
||||
folder.add_file_with_content("./.tuono/config/config.json", "INVALID JSON");
|
||||
|
||||
let config = Config::get();
|
||||
|
||||
assert!(config.is_err());
|
||||
|
||||
assert_eq!(config.err().unwrap().kind(), ErrorKind::InvalidData);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use fs_extra::dir::create_all;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TempTuonoProject {
|
||||
original_dir: PathBuf,
|
||||
#[allow(dead_code)]
|
||||
// Required for dropping the temp_dir when this struct drops
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TempTuonoProject {
|
||||
pub fn new() -> Self {
|
||||
let original_dir = env::current_dir().expect("Failed to read current_dir");
|
||||
let temp_dir = tempdir().expect("Failed to create temp_dir");
|
||||
|
||||
env::set_current_dir(temp_dir.path()).expect("Failed to change current dir into temp_dir");
|
||||
|
||||
TempTuonoProject {
|
||||
original_dir,
|
||||
temp_dir,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file_with_content<'a>(&self, path: &'a str, content: &'a str) {
|
||||
let path = PathBuf::from(path);
|
||||
create_all(
|
||||
path.parent().expect("File path does not have any parent"),
|
||||
false,
|
||||
)
|
||||
.expect("Failed to create parent file directories");
|
||||
|
||||
let mut file = File::create(path).expect("Failed to create the file");
|
||||
file.write_all(content.as_bytes())
|
||||
.expect("Failed to write into the file");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempTuonoProject {
|
||||
fn drop(&mut self) {
|
||||
// Set back the current dir in the previous state
|
||||
env::set_current_dir(&self.original_dir)
|
||||
.expect("Failed to restore the original directory.");
|
||||
}
|
||||
}
|
||||
+20
-12
@@ -1,11 +1,12 @@
|
||||
[package]
|
||||
name = "tuono_lib"
|
||||
version = "0.8.4"
|
||||
version = "0.17.9"
|
||||
edition = "2021"
|
||||
authors = ["V. Ageno <valerioageno@yahoo.it>"]
|
||||
description = "The react/rust fullstack framework"
|
||||
description = "Superfast React fullstack framework"
|
||||
homepage = "https://tuono.dev"
|
||||
keywords = [ "react", "typescript", "fullstack", "web", "ssr"]
|
||||
repository = "https://github.com/Valerioageno/tuono"
|
||||
repository = "https://github.com/tuono-labs/tuono"
|
||||
readme = "../../README.md"
|
||||
license-file = "../../LICENSE.md"
|
||||
categories = ["web-programming"]
|
||||
@@ -14,13 +15,11 @@ include = [
|
||||
"Cargo.toml"
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "tuono_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
ssr_rs = "0.5.5"
|
||||
axum = {version = "0.7.5", features = ["json", "ws"]}
|
||||
ssr_rs = "0.8.1"
|
||||
axum = {version = "0.8.1", features = ["json", "ws"]}
|
||||
axum-extra = {version = "0.10.0", features = ["cookie"]}
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
||||
erased-serde = "0.4.5"
|
||||
@@ -30,11 +29,20 @@ reqwest = {version = "0.12.4", features = ["json", "stream"]}
|
||||
once_cell = "1.19.0"
|
||||
regex = "1.10.5"
|
||||
either = "1.13.0"
|
||||
tower-http = {version = "0.5.2", features = ["fs"]}
|
||||
tower-http = {version = "0.6.0", features = ["fs"]}
|
||||
colored = "2.1.0"
|
||||
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.8.4"}
|
||||
tuono_lib_macros = {path = "../tuono_lib_macros", version = "0.17.9"}
|
||||
tuono_internal = {path = "../tuono_internal", version = "0.17.9"}
|
||||
# Match the same version used by axum
|
||||
tokio-tungstenite = "0.21.0"
|
||||
tokio-tungstenite = "0.26.0"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||
tungstenite = "0.23.0"
|
||||
tungstenite = "0.26.0"
|
||||
http = "1.1.0"
|
||||
pin-project = "1.1.7"
|
||||
tower = "0.5.1"
|
||||
|
||||
[dev-dependencies]
|
||||
fs_extra = "1.3.0"
|
||||
tempfile = "3.14.0"
|
||||
serial_test = "0.10.0"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use tuono_internal::config::Config;
|
||||
|
||||
pub static GLOBAL_CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
@@ -1,4 +1,11 @@
|
||||
//! ## Tuono
|
||||
//! Tuono is a full-stack web framework for building React applications using Rust as the backend with a strong focus on usability and performance.
|
||||
//!
|
||||
//! You can find the full documentation at [tuono.dev](https://tuono.dev/)
|
||||
|
||||
mod catch_all;
|
||||
mod config;
|
||||
mod logger;
|
||||
mod manifest;
|
||||
mod mode;
|
||||
mod payload;
|
||||
@@ -13,10 +20,11 @@ pub use mode::Mode;
|
||||
pub use payload::Payload;
|
||||
pub use request::Request;
|
||||
pub use response::{Props, Response};
|
||||
pub use server::Server;
|
||||
pub use tuono_lib_macros::handler;
|
||||
pub use server::{tuono_internal_init_v8_platform, Server};
|
||||
pub use tuono_lib_macros::{api, handler};
|
||||
|
||||
// Re-exports
|
||||
pub use axum;
|
||||
pub use reqwest;
|
||||
pub use axum_extra::extract::cookie;
|
||||
pub use ssr_rs::Ssr;
|
||||
pub use tokio;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use colored::Colorize;
|
||||
use http::{method::Method, Request, Response};
|
||||
use pin_project::pin_project;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::time::Instant;
|
||||
use tower::{Layer, Service};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoggerLayer {}
|
||||
|
||||
impl LoggerLayer {
|
||||
pub fn new() -> Self {
|
||||
LoggerLayer {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for LoggerLayer {
|
||||
type Service = Logger<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
Logger::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Logger<S> {
|
||||
inner: S,
|
||||
}
|
||||
|
||||
impl<S> Logger<S> {
|
||||
pub fn new(inner: S) -> Self {
|
||||
Logger { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for Logger<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
|
||||
ResBody: Default,
|
||||
<S as Service<Request<ReqBody>>>::Error: Debug,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = LoggerFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
|
||||
LoggerFuture {
|
||||
future: self.inner.call(req),
|
||||
method,
|
||||
path,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pub struct LoggerFuture<F> {
|
||||
#[pin]
|
||||
future: F,
|
||||
method: Method,
|
||||
path: String,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl<F, B, E> Future for LoggerFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response<B>, E>>,
|
||||
B: Default,
|
||||
E: Debug,
|
||||
{
|
||||
type Output = Result<Response<B>, E>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let res: F::Output = match this.future.poll(cx) {
|
||||
Poll::Ready(res) => res,
|
||||
Poll::Pending => return Poll::Pending,
|
||||
};
|
||||
|
||||
if this.path.starts_with("/__tuono/data") {
|
||||
return Poll::Ready(res);
|
||||
}
|
||||
|
||||
let status_code = res.as_ref().unwrap().status();
|
||||
|
||||
println!(
|
||||
" {} {} {} in {}ms",
|
||||
this.method,
|
||||
this.path,
|
||||
status_code.as_str().green(),
|
||||
this.start.elapsed().as_millis()
|
||||
);
|
||||
|
||||
Poll::Ready(res)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user