├── README.md └── createcomponent.py /README.md: -------------------------------------------------------------------------------- 1 | Video: [https://www.youtube.com/watch?v=zU1kf3Qjtdk](https://www.youtube.com/watch?v=zU1kf3Qjtdk) 2 | 3 | Fast TypeScript react component generator written in Python 3.10. 4 | 5 | For usage you need `python3.10` in your `$PATH` environment variable. 6 | 7 | Usage: 8 | 9 | ```bash 10 | ./createcomponent.py 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /createcomponent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | """Хелпер для создания React компонентов. 3 | Спрашивает, создаём компонент в components или pages, 4 | затем спрашивает, какой элемент создаём. 5 | 6 | Если для pages элемент указываем MyComponent, создаст: 7 | pages/MyComponent/MyComponent.tsx 8 | pages/MyComponent/MyComponent.module.css 9 | pages/MyComponent/index.ts (опционально) 10 | 11 | Если указать MyComponent/Element, создаст: 12 | pages/MyComponent/Element.tsx 13 | pages/MyComponent/Element.module.css 14 | pages/MyComponent/index.ts (опционально) 15 | """ 16 | from abc import abstractmethod, ABC 17 | from dataclasses import dataclass 18 | from pathlib import Path 19 | from typing import Literal, TypeAlias, Iterable 20 | 21 | 22 | SRC_DIR = Path(__file__).parent / "src" 23 | 24 | BaseFolder: TypeAlias = Literal["components", "pages"] 25 | 26 | 27 | class Colors: 28 | HEADER = '\033[95m' 29 | OKBLUE = '\033[94m' 30 | OKCYAN = '\033[96m' 31 | OKGREEN = '\033[92m' 32 | WARNING = '\033[93m' 33 | FAIL = '\033[91m' 34 | ENDC = '\033[0m' 35 | BOLD = '\033[1m' 36 | UNDERLINE = '\033[4m' 37 | 38 | 39 | @dataclass 40 | class Element: 41 | full_path: Path 42 | name: str 43 | 44 | 45 | class FileCreator(ABC): 46 | def __init__(self, element: Element): 47 | self._element = element 48 | 49 | def create(self) -> None: 50 | """Creates empty file and then fill with contents""" 51 | self._create_empty_file() 52 | self._write_file_contents() 53 | 54 | def get_relative_filename(self) -> str: 55 | """Returns relative filename as str for logging""" 56 | relative_path_start_index = 1 + len(str(SRC_DIR.resolve())) 57 | result = str( 58 | self.get_absolute_filename().resolve() 59 | )[relative_path_start_index:] 60 | return result 61 | 62 | def _create_empty_file(self): 63 | """Init file if not exists""" 64 | self.get_absolute_filename().parent.mkdir(parents=True, exist_ok=True) 65 | self.get_absolute_filename().touch(exist_ok=True) 66 | 67 | @abstractmethod 68 | def get_absolute_filename(self) -> Path: 69 | """Returns file in Path format""" 70 | pass 71 | 72 | @abstractmethod 73 | def _write_file_contents(self) -> None: 74 | """Fill file with contents""" 75 | pass 76 | 77 | 78 | class TSXFileCreator(FileCreator): 79 | """Element.tsx file creator""" 80 | def get_absolute_filename(self) -> Path: 81 | return self._element.full_path / (self._element.name + ".tsx") 82 | 83 | def _write_file_contents(self): 84 | self.get_absolute_filename().write_text( 85 | f"""import styles from "./{self._element.name}.module.css" 86 | 87 | interface {self._element.name}Props {{ 88 | 89 | }} 90 | 91 | const {self._element.name} = ({{}}: {self._element.name}Props) => ( 92 |
{self._element.name}
93 | ) 94 | 95 | export default {self._element.name} 96 | """.strip()) 97 | 98 | 99 | class CSSFileCreator(FileCreator): 100 | """Element.module.css file creator""" 101 | def get_absolute_filename(self) -> Path: 102 | return self._element.full_path / (self._element.name + ".module.css") 103 | 104 | def _write_file_contents(self): 105 | pass 106 | 107 | 108 | class IndexFileCreator(FileCreator): 109 | """Optional index.ts file creator""" 110 | def get_absolute_filename(self) -> Path: 111 | return self._element.full_path / "index.ts" 112 | 113 | def _write_file_contents(self): 114 | current_file_contents = self.get_absolute_filename().read_text() 115 | if current_file_contents.strip(): 116 | return 117 | self.get_absolute_filename().write_text( 118 | f"""export {{default}} from "./{self._element.name}";""" 119 | ) 120 | 121 | 122 | class AskParams: 123 | """Ask params from user, parse it and create Element structure""" 124 | def __init__(self): 125 | self._element: Element 126 | 127 | def ask(self) -> Element: 128 | """Ask all parameters — element folder and name""" 129 | base_folder = self._ask_base_folder() 130 | self._element = self._parse_as_element( 131 | self._ask_element(base_folder), 132 | base_folder 133 | ) 134 | return self._element 135 | 136 | def _parse_as_element(self, 137 | element_str: str, 138 | base_folder: BaseFolder) -> Element: 139 | element_as_list = element_str.split("/") 140 | element_name = element_as_list[-1] 141 | if len(element_as_list) > 1: 142 | # user entered: MyCourses/AuthorCourses 143 | relative_path = "/".join(element_as_list[:-1]) 144 | else: 145 | # user entered: AuthorCourses 146 | relative_path = element_name 147 | return Element( 148 | full_path=SRC_DIR / base_folder / relative_path, 149 | name=element_name 150 | ) 151 | 152 | def ask_ok(self, filenames: Iterable[str]) -> None: 153 | filenames = "\n\t".join(filenames) 154 | while True: 155 | print(f"\nИтак, создаём файлы:\n" 156 | f"{Colors.HEADER}\t{filenames} {Colors.ENDC}\n\n") 157 | match input("Ок? [Y]/N: ").strip().lower(): 158 | case "y" | "": return 159 | case "n": exit("Ок, выходим, ничего не создал.") 160 | case _: print("Не понял, давай ещё раз.") 161 | 162 | def _ask_base_folder(self) -> BaseFolder: 163 | while True: 164 | match input("c — components, p — pages: ").strip().lower(): 165 | case "c": return "components" 166 | case "p": return "pages" 167 | case _: print("Не понял, давай ещё раз.") 168 | 169 | def _ask_element(self, base_folder: BaseFolder) -> str: 170 | return input(f"Куда кладём? {base_folder}/").strip() 171 | 172 | 173 | class ElementFilesCreator: 174 | """Handles files creation""" 175 | def __init__(self, element: Element): 176 | self._element = element 177 | self._file_creators: list[FileCreator] = [] 178 | 179 | def create(self): 180 | for file_creator in self._file_creators: 181 | file_creator.create() 182 | 183 | def register_file_creators(self, *file_creators: type[FileCreator]): 184 | for fc in file_creators: 185 | self._file_creators.append(fc( 186 | element=self._element 187 | )) 188 | 189 | def get_relative_filenames(self) -> tuple[str, ...]: 190 | return tuple(fc.get_relative_filename() for fc in self._file_creators) 191 | 192 | 193 | def main(): 194 | asker = AskParams() 195 | element = asker.ask() 196 | 197 | element_creator = ElementFilesCreator(element) 198 | element_creator.register_file_creators( 199 | TSXFileCreator, 200 | CSSFileCreator, 201 | IndexFileCreator 202 | ) 203 | asker.ask_ok(element_creator.get_relative_filenames()) 204 | element_creator.create() 205 | print(f"Всё создал и весь такой молодец!") 206 | 207 | 208 | if __name__ == "__main__": 209 | try: 210 | main() 211 | except KeyboardInterrupt: 212 | pass 213 | --------------------------------------------------------------------------------