├── .babelrc ├── .browserslistrc ├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .nvmrc ├── .vscode └── launch.json ├── Dockerfile ├── README.md ├── ant-theme-vars.less ├── assets ├── favicon.ico └── images │ ├── flow.png │ └── logo.png ├── client ├── About │ └── About.tsx ├── App.less ├── App.tsx ├── Home │ ├── Home.less │ ├── Home.tsx │ └── NotifDemo.tsx ├── Layout │ ├── AppHeader.less │ ├── AppHeader.tsx │ ├── AppLayout.less │ ├── AppLayout.tsx │ ├── AppSider.less │ ├── AppSider.tsx │ └── Routing │ │ ├── AppBreadcrumbs.tsx │ │ ├── AppRouter.tsx │ │ └── RouteConfigs.ts ├── User │ └── UserList.tsx ├── client.tsx ├── setupEnzyme.ts ├── tsconfig.client.json └── utils │ └── .gitkeep ├── docker-compose.build.yml ├── docker-compose.yml ├── index.js ├── nest-cli.json ├── package-lock.json ├── package.json ├── server ├── config.ts ├── src │ ├── api │ │ ├── api.module.ts │ │ ├── global │ │ │ ├── auth │ │ │ │ ├── auth.guard.ts │ │ │ │ └── auth.module.ts │ │ │ ├── config │ │ │ │ ├── config.service.spec.ts │ │ │ │ ├── config.service.ts │ │ │ │ └── db-config │ │ │ │ │ ├── db-config.service.spec.ts │ │ │ │ │ └── db-config.service.ts │ │ │ ├── global.module.ts │ │ │ └── index.ts │ │ └── user │ │ │ ├── controllers │ │ │ ├── v1.user.controller.spec.ts │ │ │ └── v1.user.controller.ts │ │ │ ├── services │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ │ │ └── user.module.ts │ ├── app.module.ts │ ├── main.ts │ ├── spa │ │ ├── app.controller.spec.ts │ │ ├── manifest-manager │ │ │ ├── manifest-manager.service.spec.ts │ │ │ └── manifest-manager.service.ts │ │ ├── spa.controller.ts │ │ └── spaModule.ts │ └── statics-router.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── tsconfig.test.json ├── shared ├── .gitkeep └── entities │ ├── entityBase.ts │ ├── partials │ └── name.ts │ └── user │ └── user.entity.ts ├── tsconfig.json ├── tsconfig.webpack-config.json ├── tslint.json ├── types.d.ts ├── views └── page.ejs └── webpack.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/react", 10 | "@babel/typescript" 11 | ], 12 | "plugins": [ 13 | "react-hot-loader/babel", 14 | "babel-plugin-transform-typescript-metadata", 15 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 16 | ["@babel/plugin-proposal-class-properties", { "loose": true }], 17 | "@babel/proposal-numeric-separator", 18 | "@babel/transform-runtime", 19 | "@babel/proposal-object-rest-spread", 20 | ["import", { "libraryName": "antd", "style": true }] 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 version 2 | > 2% 3 | maintained node versions 4 | not dead 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | README.md 9 | .idea 10 | .env* 11 | .nvmrc 12 | .babelrc 13 | .browserslistrc 14 | webpack.config.ts 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 4 11 | ij_continuation_indent_size = 8 12 | ij_formatter_off_tag = @formatter:off 13 | ij_formatter_on_tag = @formatter:on 14 | ij_formatter_tags_enabled = false 15 | ij_smart_tabs = false 16 | ij_wrap_on_typing = false 17 | 18 | [*.css] 19 | ij_css_align_closing_brace_with_properties = false 20 | ij_css_blank_lines_around_nested_selector = 1 21 | ij_css_blank_lines_between_blocks = 1 22 | ij_css_brace_placement = 0 23 | ij_css_hex_color_long_format = false 24 | ij_css_hex_color_lower_case = false 25 | ij_css_hex_color_short_format = false 26 | ij_css_hex_color_upper_case = false 27 | ij_css_keep_blank_lines_in_code = 2 28 | ij_css_keep_indents_on_empty_lines = false 29 | ij_css_keep_single_line_blocks = false 30 | ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow 31 | ij_css_space_after_colon = true 32 | ij_css_space_before_opening_brace = true 33 | ij_css_value_alignment = 0 34 | 35 | [*.less] 36 | indent_size = 2 37 | ij_less_align_closing_brace_with_properties = false 38 | ij_less_blank_lines_around_nested_selector = 1 39 | ij_less_blank_lines_between_blocks = 1 40 | ij_less_brace_placement = 0 41 | ij_less_hex_color_long_format = false 42 | ij_less_hex_color_lower_case = false 43 | ij_less_hex_color_short_format = false 44 | ij_less_hex_color_upper_case = false 45 | ij_less_keep_blank_lines_in_code = 2 46 | ij_less_keep_indents_on_empty_lines = false 47 | ij_less_keep_single_line_blocks = false 48 | ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow 49 | ij_less_space_after_colon = true 50 | ij_less_space_before_opening_brace = true 51 | ij_less_value_alignment = 0 52 | 53 | [.editorconfig] 54 | ij_editorconfig_align_group_field_declarations = false 55 | ij_editorconfig_space_after_colon = false 56 | ij_editorconfig_space_after_comma = true 57 | ij_editorconfig_space_before_colon = false 58 | ij_editorconfig_space_before_comma = false 59 | ij_editorconfig_spaces_around_assignment_operators = true 60 | 61 | [{*.ats, *.ts}] 62 | ij_continuation_indent_size = 4 63 | ij_typescript_align_imports = false 64 | ij_typescript_align_multiline_array_initializer_expression = false 65 | ij_typescript_align_multiline_binary_operation = false 66 | ij_typescript_align_multiline_chained_methods = false 67 | ij_typescript_align_multiline_extends_list = false 68 | ij_typescript_align_multiline_for = true 69 | ij_typescript_align_multiline_parameters = true 70 | ij_typescript_align_multiline_parameters_in_calls = false 71 | ij_typescript_align_multiline_ternary_operation = false 72 | ij_typescript_align_object_properties = 0 73 | ij_typescript_align_union_types = false 74 | ij_typescript_align_var_statements = 0 75 | ij_typescript_array_initializer_new_line_after_left_brace = false 76 | ij_typescript_array_initializer_right_brace_on_new_line = false 77 | ij_typescript_array_initializer_wrap = off 78 | ij_typescript_assignment_wrap = off 79 | ij_typescript_binary_operation_sign_on_next_line = false 80 | ij_typescript_binary_operation_wrap = off 81 | ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**/*, @angular/material, @angular/material/typings/** 82 | ij_typescript_blank_lines_after_imports = 1 83 | ij_typescript_blank_lines_around_class = 1 84 | ij_typescript_blank_lines_around_field = 0 85 | ij_typescript_blank_lines_around_field_in_interface = 0 86 | ij_typescript_blank_lines_around_function = 1 87 | ij_typescript_blank_lines_around_method = 1 88 | ij_typescript_blank_lines_around_method_in_interface = 1 89 | ij_typescript_block_brace_style = end_of_line 90 | ij_typescript_call_parameters_new_line_after_left_paren = false 91 | ij_typescript_call_parameters_right_paren_on_new_line = false 92 | ij_typescript_call_parameters_wrap = off 93 | ij_typescript_catch_on_new_line = false 94 | ij_typescript_chained_call_dot_on_new_line = true 95 | ij_typescript_class_brace_style = end_of_line 96 | ij_typescript_comma_on_new_line = false 97 | ij_typescript_do_while_brace_force = never 98 | ij_typescript_else_on_new_line = false 99 | ij_typescript_enforce_trailing_comma = keep 100 | ij_typescript_extends_keyword_wrap = off 101 | ij_typescript_extends_list_wrap = off 102 | ij_typescript_field_prefix = _ 103 | ij_typescript_file_name_style = relaxed 104 | ij_typescript_finally_on_new_line = false 105 | ij_typescript_for_brace_force = never 106 | ij_typescript_for_statement_new_line_after_left_paren = false 107 | ij_typescript_for_statement_right_paren_on_new_line = false 108 | ij_typescript_for_statement_wrap = off 109 | ij_typescript_force_quote_style = false 110 | ij_typescript_force_semicolon_style = false 111 | ij_typescript_function_expression_brace_style = end_of_line 112 | ij_typescript_if_brace_force = never 113 | ij_typescript_import_merge_members = global 114 | ij_typescript_import_prefer_absolute_path = global 115 | ij_typescript_import_sort_members = true 116 | ij_typescript_import_sort_module_name = true 117 | ij_typescript_import_use_node_resolution = true 118 | ij_typescript_imports_wrap = on_every_item 119 | ij_typescript_indent_case_from_switch = true 120 | ij_typescript_indent_chained_calls = true 121 | ij_typescript_indent_package_children = 0 122 | ij_typescript_jsdoc_include_types = false 123 | ij_typescript_jsx_attribute_value = braces 124 | ij_typescript_keep_blank_lines_in_code = 2 125 | ij_typescript_keep_first_column_comment = true 126 | ij_typescript_keep_indents_on_empty_lines = false 127 | ij_typescript_keep_line_breaks = true 128 | ij_typescript_keep_simple_blocks_in_one_line = false 129 | ij_typescript_keep_simple_methods_in_one_line = false 130 | ij_typescript_line_comment_add_space = true 131 | ij_typescript_line_comment_at_first_column = false 132 | ij_typescript_method_brace_style = end_of_line 133 | ij_typescript_method_call_chain_wrap = off 134 | ij_typescript_method_parameters_new_line_after_left_paren = false 135 | ij_typescript_method_parameters_right_paren_on_new_line = false 136 | ij_typescript_method_parameters_wrap = off 137 | ij_typescript_object_literal_wrap = on_every_item 138 | ij_typescript_parentheses_expression_new_line_after_left_paren = false 139 | ij_typescript_parentheses_expression_right_paren_on_new_line = false 140 | ij_typescript_place_assignment_sign_on_next_line = false 141 | ij_typescript_prefer_as_type_cast = false 142 | ij_typescript_prefer_parameters_wrap = false 143 | ij_typescript_reformat_c_style_comments = false 144 | ij_typescript_space_after_colon = true 145 | ij_typescript_space_after_comma = true 146 | ij_typescript_space_after_dots_in_rest_parameter = false 147 | ij_typescript_space_after_generator_mult = true 148 | ij_typescript_space_after_property_colon = true 149 | ij_typescript_space_after_quest = true 150 | ij_typescript_space_after_type_colon = true 151 | ij_typescript_space_after_unary_not = false 152 | ij_typescript_space_before_async_arrow_lparen = true 153 | ij_typescript_space_before_catch_keyword = true 154 | ij_typescript_space_before_catch_left_brace = true 155 | ij_typescript_space_before_catch_parentheses = true 156 | ij_typescript_space_before_class_lbrace = true 157 | ij_typescript_space_before_class_left_brace = true 158 | ij_typescript_space_before_colon = true 159 | ij_typescript_space_before_comma = false 160 | ij_typescript_space_before_do_left_brace = true 161 | ij_typescript_space_before_else_keyword = true 162 | ij_typescript_space_before_else_left_brace = true 163 | ij_typescript_space_before_finally_keyword = true 164 | ij_typescript_space_before_finally_left_brace = true 165 | ij_typescript_space_before_for_left_brace = true 166 | ij_typescript_space_before_for_parentheses = true 167 | ij_typescript_space_before_for_semicolon = false 168 | ij_typescript_space_before_function_left_parenth = true 169 | ij_typescript_space_before_generator_mult = false 170 | ij_typescript_space_before_if_left_brace = true 171 | ij_typescript_space_before_if_parentheses = true 172 | ij_typescript_space_before_method_call_parentheses = false 173 | ij_typescript_space_before_method_left_brace = true 174 | ij_typescript_space_before_method_parentheses = false 175 | ij_typescript_space_before_property_colon = false 176 | ij_typescript_space_before_quest = true 177 | ij_typescript_space_before_switch_left_brace = true 178 | ij_typescript_space_before_switch_parentheses = true 179 | ij_typescript_space_before_try_left_brace = true 180 | ij_typescript_space_before_type_colon = false 181 | ij_typescript_space_before_unary_not = false 182 | ij_typescript_space_before_while_keyword = true 183 | ij_typescript_space_before_while_left_brace = true 184 | ij_typescript_space_before_while_parentheses = true 185 | ij_typescript_spaces_around_additive_operators = true 186 | ij_typescript_spaces_around_arrow_function_operator = true 187 | ij_typescript_spaces_around_assignment_operators = true 188 | ij_typescript_spaces_around_bitwise_operators = true 189 | ij_typescript_spaces_around_equality_operators = true 190 | ij_typescript_spaces_around_logical_operators = true 191 | ij_typescript_spaces_around_multiplicative_operators = true 192 | ij_typescript_spaces_around_relational_operators = true 193 | ij_typescript_spaces_around_shift_operators = true 194 | ij_typescript_spaces_around_unary_operator = false 195 | ij_typescript_spaces_within_array_initializer_brackets = false 196 | ij_typescript_spaces_within_brackets = false 197 | ij_typescript_spaces_within_catch_parentheses = false 198 | ij_typescript_spaces_within_for_parentheses = false 199 | ij_typescript_spaces_within_if_parentheses = false 200 | ij_typescript_spaces_within_imports = true 201 | ij_typescript_spaces_within_interpolation_expressions = false 202 | ij_typescript_spaces_within_method_call_parentheses = false 203 | ij_typescript_spaces_within_method_parentheses = false 204 | ij_typescript_spaces_within_object_literal_braces = true 205 | ij_typescript_spaces_within_object_type_braces = true 206 | ij_typescript_spaces_within_parentheses = false 207 | ij_typescript_spaces_within_switch_parentheses = false 208 | ij_typescript_spaces_within_type_assertion = false 209 | ij_typescript_spaces_within_union_types = true 210 | ij_typescript_spaces_within_while_parentheses = false 211 | ij_typescript_special_else_if_treatment = true 212 | ij_typescript_ternary_operation_signs_on_next_line = false 213 | ij_typescript_ternary_operation_wrap = off 214 | ij_typescript_union_types_wrap = on_every_item 215 | ij_typescript_use_chained_calls_group_indents = false 216 | ij_typescript_use_double_quotes = false 217 | ij_typescript_use_explicit_js_extension = global 218 | ij_typescript_use_path_mapping = always 219 | ij_typescript_use_public_modifier = false 220 | ij_typescript_use_semicolon_after_statement = true 221 | ij_typescript_var_declaration_wrap = normal 222 | ij_typescript_while_brace_force = never 223 | ij_typescript_while_on_new_line = false 224 | ij_typescript_wrap_comments = false 225 | 226 | [{*.cjs, *.js}] 227 | ij_continuation_indent_size = 4 228 | ij_javascript_align_imports = false 229 | ij_javascript_align_multiline_array_initializer_expression = false 230 | ij_javascript_align_multiline_binary_operation = false 231 | ij_javascript_align_multiline_chained_methods = false 232 | ij_javascript_align_multiline_extends_list = false 233 | ij_javascript_align_multiline_for = true 234 | ij_javascript_align_multiline_parameters = true 235 | ij_javascript_align_multiline_parameters_in_calls = false 236 | ij_javascript_align_multiline_ternary_operation = false 237 | ij_javascript_align_object_properties = 0 238 | ij_javascript_align_union_types = false 239 | ij_javascript_align_var_statements = 0 240 | ij_javascript_array_initializer_new_line_after_left_brace = false 241 | ij_javascript_array_initializer_right_brace_on_new_line = false 242 | ij_javascript_array_initializer_wrap = off 243 | ij_javascript_assignment_wrap = off 244 | ij_javascript_binary_operation_sign_on_next_line = false 245 | ij_javascript_binary_operation_wrap = off 246 | ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**/*, @angular/material, @angular/material/typings/** 247 | ij_javascript_blank_lines_after_imports = 1 248 | ij_javascript_blank_lines_around_class = 1 249 | ij_javascript_blank_lines_around_field = 0 250 | ij_javascript_blank_lines_around_function = 1 251 | ij_javascript_blank_lines_around_method = 1 252 | ij_javascript_block_brace_style = end_of_line 253 | ij_javascript_call_parameters_new_line_after_left_paren = false 254 | ij_javascript_call_parameters_right_paren_on_new_line = false 255 | ij_javascript_call_parameters_wrap = off 256 | ij_javascript_catch_on_new_line = false 257 | ij_javascript_chained_call_dot_on_new_line = true 258 | ij_javascript_class_brace_style = end_of_line 259 | ij_javascript_comma_on_new_line = false 260 | ij_javascript_do_while_brace_force = never 261 | ij_javascript_else_on_new_line = false 262 | ij_javascript_enforce_trailing_comma = whenmultiline 263 | ij_javascript_extends_keyword_wrap = off 264 | ij_javascript_extends_list_wrap = off 265 | ij_javascript_field_prefix = _ 266 | ij_javascript_file_name_style = relaxed 267 | ij_javascript_finally_on_new_line = false 268 | ij_javascript_for_brace_force = never 269 | ij_javascript_for_statement_new_line_after_left_paren = false 270 | ij_javascript_for_statement_right_paren_on_new_line = false 271 | ij_javascript_for_statement_wrap = off 272 | ij_javascript_force_quote_style = false 273 | ij_javascript_force_semicolon_style = false 274 | ij_javascript_function_expression_brace_style = end_of_line 275 | ij_javascript_if_brace_force = never 276 | ij_javascript_import_merge_members = global 277 | ij_javascript_import_prefer_absolute_path = global 278 | ij_javascript_import_sort_members = true 279 | ij_javascript_import_sort_module_name = true 280 | ij_javascript_import_use_node_resolution = true 281 | ij_javascript_imports_wrap = split_into_lines 282 | ij_javascript_indent_case_from_switch = true 283 | ij_javascript_indent_chained_calls = true 284 | ij_javascript_indent_package_children = 0 285 | ij_javascript_jsx_attribute_value = braces 286 | ij_javascript_keep_blank_lines_in_code = 2 287 | ij_javascript_keep_first_column_comment = true 288 | ij_javascript_keep_indents_on_empty_lines = false 289 | ij_javascript_keep_line_breaks = true 290 | ij_javascript_keep_simple_blocks_in_one_line = false 291 | ij_javascript_keep_simple_methods_in_one_line = true 292 | ij_javascript_line_comment_add_space = true 293 | ij_javascript_line_comment_at_first_column = false 294 | ij_javascript_method_brace_style = end_of_line 295 | ij_javascript_method_call_chain_wrap = off 296 | ij_javascript_method_parameters_new_line_after_left_paren = false 297 | ij_javascript_method_parameters_right_paren_on_new_line = false 298 | ij_javascript_method_parameters_wrap = off 299 | ij_javascript_object_literal_wrap = on_every_item 300 | ij_javascript_parentheses_expression_new_line_after_left_paren = false 301 | ij_javascript_parentheses_expression_right_paren_on_new_line = false 302 | ij_javascript_place_assignment_sign_on_next_line = false 303 | ij_javascript_prefer_as_type_cast = false 304 | ij_javascript_prefer_parameters_wrap = false 305 | ij_javascript_reformat_c_style_comments = false 306 | ij_javascript_space_after_colon = true 307 | ij_javascript_space_after_comma = true 308 | ij_javascript_space_after_dots_in_rest_parameter = false 309 | ij_javascript_space_after_generator_mult = true 310 | ij_javascript_space_after_property_colon = true 311 | ij_javascript_space_after_quest = true 312 | ij_javascript_space_after_type_colon = true 313 | ij_javascript_space_after_unary_not = false 314 | ij_javascript_space_before_async_arrow_lparen = true 315 | ij_javascript_space_before_catch_keyword = true 316 | ij_javascript_space_before_catch_left_brace = true 317 | ij_javascript_space_before_catch_parentheses = true 318 | ij_javascript_space_before_class_lbrace = true 319 | ij_javascript_space_before_class_left_brace = true 320 | ij_javascript_space_before_colon = true 321 | ij_javascript_space_before_comma = false 322 | ij_javascript_space_before_do_left_brace = true 323 | ij_javascript_space_before_else_keyword = true 324 | ij_javascript_space_before_else_left_brace = true 325 | ij_javascript_space_before_finally_keyword = true 326 | ij_javascript_space_before_finally_left_brace = true 327 | ij_javascript_space_before_for_left_brace = true 328 | ij_javascript_space_before_for_parentheses = true 329 | ij_javascript_space_before_for_semicolon = false 330 | ij_javascript_space_before_function_left_parenth = true 331 | ij_javascript_space_before_generator_mult = false 332 | ij_javascript_space_before_if_left_brace = true 333 | ij_javascript_space_before_if_parentheses = true 334 | ij_javascript_space_before_method_call_parentheses = false 335 | ij_javascript_space_before_method_left_brace = true 336 | ij_javascript_space_before_method_parentheses = false 337 | ij_javascript_space_before_property_colon = false 338 | ij_javascript_space_before_quest = true 339 | ij_javascript_space_before_switch_left_brace = true 340 | ij_javascript_space_before_switch_parentheses = true 341 | ij_javascript_space_before_try_left_brace = true 342 | ij_javascript_space_before_type_colon = false 343 | ij_javascript_space_before_unary_not = false 344 | ij_javascript_space_before_while_keyword = true 345 | ij_javascript_space_before_while_left_brace = true 346 | ij_javascript_space_before_while_parentheses = true 347 | ij_javascript_spaces_around_additive_operators = true 348 | ij_javascript_spaces_around_arrow_function_operator = true 349 | ij_javascript_spaces_around_assignment_operators = true 350 | ij_javascript_spaces_around_bitwise_operators = true 351 | ij_javascript_spaces_around_equality_operators = true 352 | ij_javascript_spaces_around_logical_operators = true 353 | ij_javascript_spaces_around_multiplicative_operators = true 354 | ij_javascript_spaces_around_relational_operators = true 355 | ij_javascript_spaces_around_shift_operators = true 356 | ij_javascript_spaces_around_unary_operator = false 357 | ij_javascript_spaces_within_array_initializer_brackets = false 358 | ij_javascript_spaces_within_brackets = false 359 | ij_javascript_spaces_within_catch_parentheses = false 360 | ij_javascript_spaces_within_for_parentheses = false 361 | ij_javascript_spaces_within_if_parentheses = false 362 | ij_javascript_spaces_within_imports = true 363 | ij_javascript_spaces_within_interpolation_expressions = false 364 | ij_javascript_spaces_within_method_call_parentheses = false 365 | ij_javascript_spaces_within_method_parentheses = false 366 | ij_javascript_spaces_within_object_literal_braces = true 367 | ij_javascript_spaces_within_object_type_braces = true 368 | ij_javascript_spaces_within_parentheses = false 369 | ij_javascript_spaces_within_switch_parentheses = false 370 | ij_javascript_spaces_within_type_assertion = false 371 | ij_javascript_spaces_within_union_types = true 372 | ij_javascript_spaces_within_while_parentheses = false 373 | ij_javascript_special_else_if_treatment = true 374 | ij_javascript_ternary_operation_signs_on_next_line = false 375 | ij_javascript_ternary_operation_wrap = off 376 | ij_javascript_union_types_wrap = on_every_item 377 | ij_javascript_use_chained_calls_group_indents = false 378 | ij_javascript_use_double_quotes = false 379 | ij_javascript_use_explicit_js_extension = global 380 | ij_javascript_use_path_mapping = always 381 | ij_javascript_use_public_modifier = false 382 | ij_javascript_use_semicolon_after_statement = true 383 | ij_javascript_var_declaration_wrap = normal 384 | ij_javascript_while_brace_force = never 385 | ij_javascript_while_on_new_line = false 386 | ij_javascript_wrap_comments = false 387 | 388 | [{*.html, *.sht, *.htm, *.shtm, *.shtml, *.ng}] 389 | ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 390 | ij_html_align_attributes = true 391 | ij_html_align_text = false 392 | ij_html_attribute_wrap = normal 393 | ij_html_block_comment_at_first_column = true 394 | ij_html_do_not_align_children_of_min_lines = 0 395 | ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p 396 | ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot 397 | ij_html_enforce_quotes = false 398 | ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var 399 | ij_html_keep_blank_lines = 2 400 | ij_html_keep_indents_on_empty_lines = false 401 | ij_html_keep_line_breaks = true 402 | ij_html_keep_line_breaks_in_text = true 403 | ij_html_keep_whitespaces = false 404 | ij_html_keep_whitespaces_inside = span, pre, textarea 405 | ij_html_line_comment_at_first_column = true 406 | ij_html_new_line_after_last_attribute = never 407 | ij_html_new_line_before_first_attribute = never 408 | ij_html_quote_style = double 409 | ij_html_remove_new_line_before_tags = br 410 | ij_html_space_after_tag_name = false 411 | ij_html_space_around_equality_in_attribute = false 412 | ij_html_space_inside_empty_tag = true 413 | ij_html_text_wrap = normal 414 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_PORT=3001 2 | DB_HOST=localhost 3 | DB_PORT=54320 4 | DB_USERNAME=dev 5 | DB_PASSWORD=password 6 | DB_DATABASE=card-manager 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .env 3 | .idea 4 | node_modules 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.16.0 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Attach to Chrome", 11 | "port": 9222, 12 | "urlFilter": "http://localhost:3001/*", 13 | "webRoot": "${workspaceFolder}" 14 | }, 15 | { 16 | "name": "Launch localhost", 17 | "type": "chrome", 18 | "request": "launch", 19 | "url": "http://localhost:3001", 20 | "webRoot": "${workspaceFolder}/client", 21 | "sourceMapPathOverrides": { 22 | "webpack:///./client/*": "${webRoot}/*", 23 | "webpack:///client/*": "${webRoot}/*", 24 | "webpack:///*": "*", 25 | "webpack:///./~/*": "${webRoot}/node_modules/*" 26 | } 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Webpack", 32 | "program": "${workspaceFolder}/node_modules/.bin/webpack-cli", 33 | "args": [ 34 | "--config", 35 | "webpack.config.ts" 36 | ], 37 | "autoAttachChildProcesses": true, 38 | "runtimeExecutable": "~/.nvm/versions/node/v10.16.3/bin/node" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | ENV NODE_ENV=production 4 | 5 | WORKDIR /usr/src/app 6 | 7 | COPY package*.json ./ 8 | RUN npm install --production 9 | 10 | COPY ./dist ./dist 11 | COPY ./index.js . 12 | 13 | CMD [ "npm", "run", "prod" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Nest Logo 4 |

5 |

Fullstack React/Typescript/NestJs starter, heavily based off of fullstack-typescript by gilamran.

6 | 7 | --- 8 | 9 | ### Quick Start 10 | 11 | Just fork the repo and then clone your forked repository into your own project folder. 12 | 13 | ``` 14 | git clone 15 | cd 16 | npm install 17 | cp .env.example .env 18 | docker-compose up 19 | npm start 20 | ``` 21 | 22 | If you want, you can just clone and detach from this repository into your own repository do this: 23 | 24 | ``` 25 | git clone https://github.com/heuristicAL/fullstack-ts-react-nest.git 26 | cd 27 | git remote remove origin 28 | git remote add origin YOUR_REPO_URL 29 | git push -u origin master 30 | ``` 31 | 32 | ## Why 33 | 34 | - **Simple** to jump into, **Fast** because it is simple. 35 | - Separate `tsconfig.json` for client and server. 36 | - Client and server can share code (And types). 37 | - The client is bundled using [Webpack](https://webpack.github.io/) because it goes to the browser. 38 | - The server is emitted by [TypeScript](https://github.com/Microsoft/TypeScript) because node 6 supports es6. 39 | 40 |

41 | 42 |

43 | 44 | --- 45 | 46 | ### Directory Layout 47 | 48 | ``` 49 | . 50 | ├── /node_modules/ # 3rd-party libraries and utilities 51 | ├── /dist/ # All the generated files will go here, and will run from this folder 52 | ├── /assets/ # images, css, jsons etc. 53 | ├── /client/ # React app 54 | ├── /server/ # Express server app. 55 | ├── /spa/ # Global path that loads the client React app. 56 | ├── /api/ # Api REST endpoints. 57 | ├── /shared/ # The shared code between the client and the server 58 | ├── /views/ # the views for the server (currently only loads the react app) 59 | ├── .babelrc # babel configuration 60 | ├── .gitignore # ignored git files and folders 61 | ├── .nvmrc # Force nodejs version 62 | ├── .env # (ignored) Can be used to override environment variables 63 | ├── docker-compose.build.yml # docker-compose for building prod image 64 | ├── docker-compose.yml # docker-compose for running dependencies locally 65 | ├── package.json # The list of 3rd party libraries and utilities 66 | └── tslint.json # TypeScript linting configuration file 67 | └── tsconfig.json # TypeScript configuration file 68 | └── tsconfig.webpack-config.json # TypeScript configuration file 69 | ├── README.md # This file 70 | ``` 71 | 72 | ### What's included 73 | 74 | - [React v16](https://facebook.github.io/react/) 75 | - [React router v5](https://github.com/ReactTraining/react-router) 76 | - [Ant Design](https://ant.design/) 77 | - [Jest](https://github.com/facebook/jest) 78 | - [Css modules](https://github.com/css-modules/css-modules) 79 | - [Axios-Observable](https://github.com/zhaosiyang/axios-observable) (For Client/Server communication) 80 | - [NestJs](https://github.com/nestjs/nest) 81 | 82 | ### Usage 83 | > Before running any of the following commands, you should cope `.env.example` to `.env` in the root directory, substituting in your own variables.
84 | > **Read the docker commands below to setup a local DB for the server.** 85 | 86 | - `npm start` - Client and server are in watch mode with source maps, opens [http://localhost:3000](http://localhost:3000) 87 | - `npm run test` - Runs jest tests 88 | - `npm run build` - `dist` folder will include all the needed files, both client (Bundle) and server. 89 | - `npm run prod` - Just runs `node ./dist/server/server.js`, run `npm run build` before this. 90 | 91 | #### Docker commands 92 | 93 | - `docker-compose up` - Spins up a local Postgres image, reading environment variables from the root `.env` file. 94 | - `docker-compose -f ./docker-compose.build.yml build` - Builds a production docker image for the app. **Run the npm build script first** 95 | - `docker-compose -f ./docker-compose.build.yml up` - Spins up the production docker container. Builds it first if it has not been built yet. 96 | 97 | ### Config 98 | 99 | All applications require a config mechanism, for example, `SLACK_API_TOKEN`. Things that you don't want in your git history, you want a different environment to have different value (dev/staging/production). This repo uses the file `config.ts` to access and setup all pre-nest init app variables. And a `.env` file to override variable in dev environment. This file is ignored from git. 100 | Once Nest is up and running, there is a [ConfigService](https://github.com/heuristicAL/fullstack-ts-react-nest/blob/master/server/src/config/config.service.ts) which can be injected on-demand. 101 | 102 | --- 103 | 104 | #### What's not included 105 | 106 | - Universal (Server side rendering) 107 | - Redux/MobX (State management) 108 | 109 | #### Requirements 110 | 111 | - Node 6+ 112 | 113 | --- 114 | 115 | #### Licence 116 | 117 | This code is released as is, under MIT licence. Feel free to use it for free for both commercial and private projects. No warranty provided. 118 | -------------------------------------------------------------------------------- /ant-theme-vars.less: -------------------------------------------------------------------------------- 1 | // ant-default-vars.less 2 | // Available theme variables can be found in 3 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less 4 | 5 | @primary-color: #1890ff; // primary color for all components 6 | @link-color: #1890ff; // link color 7 | @success-color: #52c41a; // success state color 8 | @warning-color: #faad14; // warning state color 9 | @error-color: #f5222d; // error state color 10 | @font-size-base: 14px; // major text font size 11 | @heading-color: rgba(0, 0, 0, 0.85); // heading text color 12 | @text-color: rgba(0, 0, 0, 0.65); // major text color 13 | @text-color-secondary : rgba(0, 0, 0, .45); // secondary text color 14 | @disabled-color : rgba(0, 0, 0, .25); // disable state color 15 | @border-radius-base: 4px; // major border radius 16 | @border-color-base: #d9d9d9; // major border color 17 | @box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // major shadow for layers 18 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/assets/favicon.ico -------------------------------------------------------------------------------- /assets/images/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/assets/images/flow.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/assets/images/logo.png -------------------------------------------------------------------------------- /client/About/About.tsx: -------------------------------------------------------------------------------- 1 | import { Card } from 'antd'; 2 | import Typography from 'antd/lib/typography'; 3 | import React from 'react'; 4 | 5 | const {Paragraph} = Typography; 6 | 7 | export const About: React.FC = () => { 8 | return ( 9 | 10 | 11 | Heavily Modified to NestJs by Alexandre Giard 12 | 13 | 14 | You can find information at{' '} 15 | https://github.com/gilamran/fullstack-typescript 16 | 17 | 18 | Or, for this specific starter{' '} 19 | https://github.com/heuristicAL/fullstack-ts-react-nest 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /client/App.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/client/App.less -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import { notification } from 'antd'; 2 | import React from 'react'; 3 | import { hot } from 'react-hot-loader'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | import './App.less'; 7 | import { AppLayout } from './Layout/AppLayout'; 8 | // Pages 9 | 10 | notification.config({ 11 | placement: 'topRight', 12 | top: 72, 13 | duration: 5, 14 | }); 15 | 16 | const AppImpl: React.FC = () => { 17 | return ( 18 | 19 |
20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export const App = hot(module)(AppImpl); 27 | -------------------------------------------------------------------------------- /client/Home/Home.less: -------------------------------------------------------------------------------- 1 | .logo { 2 | width: 150px; 3 | padding-bottom: 25px; 4 | } 5 | -------------------------------------------------------------------------------- /client/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Col, Row } from 'antd'; 2 | import Typography from 'antd/lib/typography'; 3 | import React from 'react'; 4 | 5 | import styles from './Home.less'; 6 | import { NotifDemo } from './NotifDemo'; 7 | 8 | const { Text } = Typography; 9 | const logoImg = require('../../assets/images/logo.png'); 10 | 11 | export const Home: React.FC = () => { 12 | return ( 13 | 15 | 16 | 17 | 18 | 19 | This is a starter kit to get you up and running with React & 20 | TypeScript on top of material-ui. 21 | 22 | 23 | You can read more about how to share code between the client and the 24 | server at my{' '} 25 | 26 | medium blog post 27 | 28 | . 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /client/Home/NotifDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, notification } from 'antd'; 2 | import React from 'react'; 3 | 4 | export const NotifDemo: React.FunctionComponent = () => { 5 | 6 | const openNotificationWithIcon = type => { 7 | notification[type]({ 8 | message: 'Notification Title', 9 | description: 10 | 'This is the content of the notification. This is the content of the notification. This is the content of the notification.', 11 | }); 12 | }; 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /client/Layout/AppHeader.less: -------------------------------------------------------------------------------- 1 | .trigger { 2 | font-size: 18px; 3 | line-height: 64px; 4 | padding: 0 24px; 5 | cursor: pointer; 6 | transition: color 0.3s; 7 | } 8 | 9 | .trigger:hover { 10 | color: #1890ff; 11 | } 12 | -------------------------------------------------------------------------------- /client/Layout/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Layout } from 'antd'; 2 | import React from 'react'; 3 | import styles from './AppHeader.less'; 4 | import { ILayoutProps } from './AppLayout'; 5 | 6 | const { Header } = Layout; 7 | 8 | export const AppHeader: React.FC = props => { 9 | 10 | const { 11 | collapsed: [drawerCollapsed, setDrawerCollapsed] 12 | } = props.state.drawer; 13 | 14 | const toggle = () => { 15 | setDrawerCollapsed(!drawerCollapsed); 16 | }; 17 | 18 | return ( 19 |
20 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /client/Layout/AppLayout.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/client/Layout/AppLayout.less -------------------------------------------------------------------------------- /client/Layout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import React from 'react'; 3 | import { Dispatch, SetStateAction } from 'react'; 4 | import { AppHeader } from './AppHeader'; 5 | import { AppSider } from './AppSider'; 6 | import { AppBreadcrumbs } from './Routing/AppBreadcrumbs'; 7 | import { AppRouter } from './Routing/AppRouter'; 8 | 9 | const {Content, Footer} = Layout; 10 | 11 | export interface ILayoutProps { 12 | state: { 13 | drawer: { 14 | collapsed: [boolean, Dispatch>], 15 | collapsedWidth: [number, Dispatch>] 16 | }; 17 | }; 18 | } 19 | 20 | export const AppLayout: React.FC = () => { 21 | const drawerState = { 22 | drawer: { 23 | collapsed: React.useState(false), 24 | collapsedWidth: React.useState(80) 25 | } 26 | }; 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
©2019 Created by Alexandre Giard
38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /client/Layout/AppSider.less: -------------------------------------------------------------------------------- 1 | .logo { 2 | height: 32px; 3 | background: rgba(255, 255, 255, 0.2); 4 | margin: 16px; 5 | } 6 | -------------------------------------------------------------------------------- /client/Layout/AppSider.tsx: -------------------------------------------------------------------------------- 1 | import { Icon, Layout, Menu } from 'antd'; 2 | import React, { FC, useEffect, useState } from 'react'; 3 | import { matchRoutes } from 'react-router-config'; 4 | import { NavLink, withRouter } from 'react-router-dom'; 5 | import { ILayoutProps } from './AppLayout'; 6 | import styles from './AppSider.less'; 7 | import { routesConfig } from './Routing/RouteConfigs'; 8 | 9 | const { Sider } = Layout; 10 | const { SubMenu } = Menu; 11 | 12 | export const AppSider: FC = withRouter(props => { 13 | const { 14 | collapsed: [drawerCollapsed, setDrawerCollapsed], 15 | collapsedWidth: [drawerCollapsedWidth, setDrawerCollapsedWidth] 16 | } = props.state.drawer; 17 | const location = props.location; 18 | const [selectedKeys, setSelectedKeys] = useState(['/']); 19 | 20 | const onCollapse = collapsed => { 21 | setDrawerCollapsed(collapsed); 22 | }; 23 | 24 | const onBreakpoint = breakpoint => { 25 | setDrawerCollapsedWidth(breakpoint ? 0 : 80); 26 | }; 27 | 28 | useEffect(() => { 29 | // Get the route info 30 | const branch = matchRoutes(routesConfig, location.pathname); 31 | const selectedRoutes = branch.map(r => r.match.url); 32 | setSelectedKeys(selectedRoutes); 33 | }, [location.pathname]); 34 | 35 | return ( 36 | 43 |
44 | 48 | 49 | 50 | 51 | Home 52 | 53 | 54 | 55 | 56 | 57 | About 58 | 59 | 60 | 61 | 62 | 63 | Users 64 | 65 | 66 | 70 | 71 | Admin 72 | 73 | } 74 | > 75 | 76 | 77 | 78 | Nothing 79 | 80 | 81 | 82 | 83 | 84 | Should 85 | 86 | 87 | 88 | 89 | 90 | Happen 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | }); 98 | -------------------------------------------------------------------------------- /client/Layout/Routing/AppBreadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from 'antd'; 2 | import React, { ComponentClass } from 'react'; 3 | import withBreadcrumbs, { BreadcrumbsRoute } from 'react-router-breadcrumbs-hoc'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | import { routesConfig } from './RouteConfigs'; 7 | 8 | export const AppBreadcrumbs: ComponentClass = withBreadcrumbs(routesConfig as BreadcrumbsRoute[])(({ breadcrumbs }) => { 9 | return ( 10 | 11 | {breadcrumbs.map(({ 12 | match, 13 | breadcrumb 14 | // other props are available during render, such as `location` 15 | // and any props found in your route objects will be passed through too 16 | }) => ( 17 | 18 | {breadcrumb} 19 | 20 | ))} 21 | 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /client/Layout/Routing/AppRouter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router-dom'; 3 | import { renderRoutes } from 'react-router-config'; 4 | import { routesConfig } from './RouteConfigs'; 5 | 6 | export const AppRouter: React.FC = () => { 7 | return ( 8 |
9 | 10 | {renderRoutes(routesConfig)} 11 | 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /client/Layout/Routing/RouteConfigs.ts: -------------------------------------------------------------------------------- 1 | import { BreadcrumbsRoute } from 'react-router-breadcrumbs-hoc'; 2 | import { RouteConfig } from 'react-router-config'; 3 | import { About } from '../../About/About'; 4 | import { Home } from '../../Home/Home'; 5 | import { UserList } from '../../User/UserList'; 6 | 7 | export const routesConfig: Array> = [ 8 | { 9 | path: '/', 10 | component: Home, 11 | exact: true, 12 | breadcrumb: 'Home' 13 | }, 14 | { 15 | path: '/about', 16 | component: About 17 | }, 18 | { 19 | path: '/users', 20 | component: UserList, 21 | breadcrumb: 'Users', 22 | routes: [ 23 | { 24 | path: '/users/:userName' 25 | } 26 | ] 27 | } 28 | ]; 29 | -------------------------------------------------------------------------------- /client/User/UserList.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Table, Tag } from 'antd'; 2 | import React from 'react'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | const columns = [ 6 | { 7 | title: 'Name', 8 | dataIndex: 'name', 9 | key: 'name', 10 | render: text => {text}, 11 | }, 12 | { 13 | title: 'Age', 14 | dataIndex: 'age', 15 | key: 'age', 16 | }, 17 | { 18 | title: 'Address', 19 | dataIndex: 'address', 20 | key: 'address', 21 | }, 22 | { 23 | title: 'Tags', 24 | key: 'tags', 25 | dataIndex: 'tags', 26 | render: tags => ( 27 | 28 | {tags.map(tag => { 29 | let color = tag.length > 5 ? 'geekblue' : 'green'; 30 | if (tag === 'loser') { 31 | color = 'volcano'; 32 | } 33 | return ( 34 | 35 | {tag.toUpperCase()} 36 | 37 | ); 38 | })} 39 | 40 | ), 41 | }, 42 | { 43 | title: 'Action', 44 | key: 'action', 45 | render: (text, record) => ( 46 | 47 | Invite {record.name} 48 | 49 | Delete 50 | 51 | ), 52 | }, 53 | ]; 54 | 55 | const data = [ 56 | { 57 | key: '1', 58 | name: 'John Brown', 59 | age: 32, 60 | address: 'New York No. 1 Lake Park', 61 | tags: ['nice', 'developer'], 62 | }, 63 | { 64 | key: '2', 65 | name: 'Jim Green', 66 | age: 42, 67 | address: 'London No. 1 Lake Park', 68 | tags: ['loser'], 69 | }, 70 | { 71 | key: '3', 72 | name: 'Joe Black', 73 | age: 32, 74 | address: 'Sidney No. 1 Lake Park', 75 | tags: ['cool', 'teacher'], 76 | }, 77 | ]; 78 | 79 | export const UserList: React.FC = () => { 80 | return (); 81 | }; 82 | -------------------------------------------------------------------------------- /client/client.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Favicon from 'react-favicon'; 4 | // noinspection ES6UnusedImports 5 | import { hot } from 'react-hot-loader'; 6 | import { App } from './App'; 7 | 8 | const favicon = require('../assets/images/logo.png'); 9 | 10 | // const theme = createMuiTheme({ 11 | // palette: { 12 | // primary: indigo, 13 | // secondary: red, 14 | // } 15 | // }); 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | 22 | , document.getElementById('app')); 23 | -------------------------------------------------------------------------------- /client/setupEnzyme.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import * as EnzymeAdapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({adapter: new EnzymeAdapter()}); 5 | -------------------------------------------------------------------------------- /client/tsconfig.client.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true, 5 | "paths": { 6 | "typeorm": ["../node_modules/typeorm/typeorm-model-shim.js"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/client/utils/.gitkeep -------------------------------------------------------------------------------- /docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | cm: 5 | build: . 6 | # volumes: # Possibly only useful for developing inside docker? 7 | # - .:/usr/src/app 8 | # - /usr/src/app/node_modules 9 | ports: 10 | - ${APP_PORT}:3001 11 | - 9229:9229 12 | depends_on: 13 | - cm_db 14 | environment: 15 | DB_HOST: cm_db 16 | APP_PORT: 17 | DB_PORT: 5432 18 | DB_USERNAME: 19 | DB_PASSWORD: 20 | DB_DATABASE: 21 | cm_db: 22 | image: postgres:11.2-alpine 23 | ports: 24 | - "${DB_PORT}:5432" 25 | volumes: 26 | - dbdata:/var/lib/postgresql/data 27 | environment: 28 | POSTGRES_PASSWORD: ${DB_PASSWORD} 29 | POSTGRES_USER: ${DB_USERNAME} 30 | POSTGRES_DB: ${DB_DATABASE} 31 | 32 | volumes: 33 | dbdata: 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | cm_db: 5 | image: postgres:11.2-alpine 6 | ports: 7 | - "${DB_PORT}:5432" 8 | volumes: 9 | - dbdata:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_PASSWORD: ${DB_PASSWORD} 12 | POSTGRES_USER: ${DB_USERNAME} 13 | POSTGRES_DB: ${DB_DATABASE} 14 | 15 | volumes: 16 | dbdata: -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/server/src/main'); 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "server/src/api" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-ts-react-nest", 3 | "version": "1.0.0", 4 | "description": "FullStack React with TypeScript and NestJS starter kit.", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=6.9.5" 8 | }, 9 | "scripts": { 10 | "clean": "rimraf dist", 11 | "copy:views": "copyfiles views/*.ejs dist\n", 12 | "start": "npm run clean && npm run copy:views && concurrently --prefix \"[{name}]\" --names \"SERVER,CLIENT\" -c \"bgBlue.bold,bgGreen.bold\" \"npm run dev-server\" \"npm run dev-client\"", 13 | "start:hot": "npm run clean && npm run copy:views && concurrently --prefix \"[{name}]\" --names \"SERVER,CLIENT\" -c \"bgBlue.bold,bgGreen.bold\" \"npm run dev-server\" \"npm run dev-client:hot\"", 14 | "prestart:prod": "rimraf dist && npm run build", 15 | "start:prod": "cross-env NODE_ENV=production node index.js", 16 | "dev-client": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack-config.json\" webpack-dev-server -w", 17 | "dev-client:hot": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack-config.json\" webpack-dev-server -w --hot", 18 | "dev-server": "tsc-watch -p ./server --onSuccess \"node --inspect index.js\"", 19 | "lint": "tslint -c tslint.json 'src/**/*.ts' 'src/**/*.tsx'", 20 | "prod": "node index.js", 21 | "type-check": "tsc -p ./tsconfig.json", 22 | "build-client": "cross-env NODE_ENV=production TS_NODE_PROJECT=\"tsconfig.webpack-config.json\" webpack -p", 23 | "build-server": "tsc -p ./server", 24 | "build": "npm run clean && npm run copy:views && concurrently --prefix \"[{name}]\" --names \"SERVER,CLIENT\" -c \"bgBlue.bold,bgGreen.bold\" \"npm run build-server\" \"npm run build-client\"", 25 | "test": "jest", 26 | "test:watch": "jest --watch", 27 | "test:cov": "jest --coverage", 28 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register jest --runInBand", 29 | "test:e2e": "jest --config ./test/jest-e2e.json" 30 | }, 31 | "prettier": { 32 | "trailingComma": "all", 33 | "tabWidth": 2, 34 | "semi": true, 35 | "singleQuote": true, 36 | "jsxSingleQuote": true, 37 | "printWidth": 120 38 | }, 39 | "keywords": [ 40 | "typescript", 41 | "react", 42 | "starter-kit", 43 | "webpack", 44 | "fullstack", 45 | "express", 46 | "express4", 47 | "node" 48 | ], 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/gilamran/fullstack-typescript.git" 52 | }, 53 | "license": "MIT", 54 | "author": "Gil Amran", 55 | "jest": { 56 | "roots": [ 57 | "/server", 58 | "/client" 59 | ], 60 | "transform": { 61 | "^.+\\.tsx?$": "ts-jest" 62 | }, 63 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", 64 | "snapshotSerializers": [ 65 | "enzyme-to-json/serializer" 66 | ], 67 | "setupFilesAfterEnv": [ 68 | "/client/setupEnzyme.ts" 69 | ], 70 | "moduleFileExtensions": [ 71 | "ts", 72 | "tsx", 73 | "js", 74 | "jsx", 75 | "json", 76 | "node" 77 | ], 78 | "globals": { 79 | "ts-jest": { 80 | "tsConfig": "/server/tsconfig.test.json" 81 | } 82 | } 83 | }, 84 | "dependencies": { 85 | "@babel/polyfill": "^7.4.4", 86 | "@nestjs/common": "^6.6.7", 87 | "@nestjs/core": "^6.6.7", 88 | "@nestjs/platform-express": "^6.6.7", 89 | "@nestjs/typeorm": "^6.1.3", 90 | "@types/react-router": "^5.0.3", 91 | "@types/react-router-config": "^5.0.0", 92 | "antd": "~3.23.2", 93 | "axios-observable": "^1.1.2", 94 | "babel-plugin-react-css-modules": "^5.2.6", 95 | "babel-plugin-transform-typescript-metadata": "^0.2.2", 96 | "body-parser": "^1.19.0", 97 | "clsx": "^1.0.4", 98 | "css-modules": "^0.3.0", 99 | "date-fns": "^2.0.1", 100 | "dotenv": "^8.1.0", 101 | "ejs": "^2.7.1", 102 | "express": "^4.17.1", 103 | "find-up": "^4.1.0", 104 | "less": "~2.7.3", 105 | "less-vars-to-js": "^1.3.0", 106 | "nest-router": "^1.0.9", 107 | "pg": "^7.12.1", 108 | "react-favicon": "0.0.17", 109 | "react-router-breadcrumbs-hoc": "^3.2.3", 110 | "react-router-config": "^5.0.1", 111 | "reflect-metadata": "^0.1.13", 112 | "rxjs": "^6.5.3", 113 | "tsconfig-paths-webpack-plugin": "^3.2.0", 114 | "tslib": "^1.10.0", 115 | "typeorm": "^0.2.18" 116 | }, 117 | "devDependencies": { 118 | "@babel/core": "^7.5.5", 119 | "@babel/plugin-proposal-class-properties": "^7.5.5", 120 | "@babel/plugin-proposal-decorators": "^7.6.0", 121 | "@babel/plugin-proposal-numeric-separator": "^7.2.0", 122 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 123 | "@babel/plugin-transform-runtime": "^7.5.5", 124 | "@babel/preset-env": "^7.5.5", 125 | "@babel/preset-react": "^7.0.0", 126 | "@babel/preset-typescript": "^7.3.3", 127 | "@hot-loader/react-dom": "^16.9.0", 128 | "@nestjs/testing": "^6.6.7", 129 | "@types/copy-webpack-plugin": "^5.0.0", 130 | "@types/cssnano": "^4.0.0", 131 | "@types/enzyme": "^3.10.3", 132 | "@types/express": "^4.17.1", 133 | "@types/jest": "^24.0.18", 134 | "@types/node": "^10.14.17", 135 | "@types/react": "^16.9.2", 136 | "@types/supertest": "^2.0.8", 137 | "@types/webpack": "^4.39.1", 138 | "@types/webpack-bundle-analyzer": "^2.13.2", 139 | "@types/webpack-dev-server": "^3.1.7", 140 | "@types/webpack-manifest-plugin": "^2.0.0", 141 | "babel-loader": "^8.0.6", 142 | "babel-plugin-import": "^1.12.1", 143 | "concurrently": "^4.1.2", 144 | "copy-webpack-plugin": "^5.0.4", 145 | "copyfiles": "^2.1.1", 146 | "cross-env": "^5.2.1", 147 | "css-loader": "^2.1.1", 148 | "cssnano": "^4.1.10", 149 | "enzyme": "^3.10.0", 150 | "enzyme-adapter-react-16": "^1.14.0", 151 | "enzyme-to-json": "^3.4.0", 152 | "file-loader": "^4.2.0", 153 | "http-proxy-middleware": "^0.20.0", 154 | "jest": "^24.9.0", 155 | "less-loader": "^5.0.0", 156 | "open-browser-webpack-plugin": "^0.0.5", 157 | "postcss-loader": "^3.0.0", 158 | "react": "^16.9.0", 159 | "react-dom": "^16.9.0", 160 | "react-hot-loader": "^4.12.12", 161 | "react-router": "^5.0.1", 162 | "react-router-dom": "^5.0.1", 163 | "rimraf": "^3.0.0", 164 | "style-loader": "^1.0.0", 165 | "ts-jest": "^24.0.2", 166 | "ts-node": "^8.3.0", 167 | "tsc-watch": "^3.0.0", 168 | "tsconfig-paths": "^3.8.0", 169 | "tslint": "^5.19.0", 170 | "typescript": "~3.5.3", 171 | "url-loader": "^2.1.0", 172 | "webpack": "^4.39.3", 173 | "webpack-bundle-analyzer": "^3.4.1", 174 | "webpack-cli": "^3.3.7", 175 | "webpack-dev-server": "^3.8.0", 176 | "webpack-manifest-plugin": "^2.0.4" 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /server/config.ts: -------------------------------------------------------------------------------- 1 | const IS_DEV = process.env.NODE_ENV !== 'production'; 2 | 3 | const findUp = require('find-up'); 4 | 5 | if (IS_DEV) { 6 | require('dotenv').config({path: findUp.sync('.env')}); 7 | } 8 | 9 | const {version: VERSION} = require(findUp.sync('package.json')); 10 | 11 | // server 12 | const SERVER_PORT = process.env.APP_PORT || 3001; // TODO: Crash if port is not specified? 13 | const WEBPACK_PORT = 8086; // For dev environment only 14 | 15 | export { 16 | IS_DEV, 17 | VERSION, 18 | SERVER_PORT, 19 | WEBPACK_PORT, 20 | }; 21 | -------------------------------------------------------------------------------- /server/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { RouterModule } from 'nest-router'; 3 | import { GlobalModule } from './global'; 4 | import { UserModule } from './user/user.module'; 5 | 6 | @Module({ 7 | imports: [ 8 | RouterModule.forRoutes([{ 9 | path: '/api', 10 | module: ApiModule, 11 | children: [ 12 | UserModule 13 | ] 14 | }]), 15 | HttpModule, 16 | GlobalModule, 17 | UserModule, 18 | ], 19 | controllers: [], 20 | providers: [], 21 | 22 | }) 23 | export class ApiModule { 24 | } 25 | -------------------------------------------------------------------------------- /server/src/api/global/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class AuthGuard implements CanActivate { 6 | canActivate( 7 | context: ExecutionContext, 8 | ): boolean | Promise | Observable { 9 | const request = context.switchToHttp().getRequest(); 10 | // This just always returns true as the request object should always be present. 11 | return !!request; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/src/api/global/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { AuthGuard } from './auth.guard'; 4 | 5 | @Module({ 6 | imports: [], 7 | controllers: [], 8 | providers: [{ 9 | provide: APP_GUARD, 10 | useClass: AuthGuard, 11 | }], 12 | }) 13 | export class AuthModule { 14 | } 15 | -------------------------------------------------------------------------------- /server/src/api/global/config/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ConfigService } from './config.service'; 3 | 4 | describe('ConfigService', () => { 5 | let service: ConfigService; 6 | 7 | beforeEach(async () => { 8 | process.env = {Key: 'Key_Value'}; 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ConfigService], 11 | }).compile(); 12 | 13 | service = module.get(ConfigService); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(service).toBeDefined(); 18 | }); 19 | it('should return "Key_Value"', () => { 20 | expect(service.getString('Key')).toBe('Key_Value'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /server/src/api/global/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import * as dotenv from 'dotenv'; 4 | import * as findUp from 'find-up'; 5 | 6 | const IS_DEV = process.env.NODE_ENV !== 'production'; 7 | 8 | @Injectable() 9 | export class ConfigService { 10 | private readonly envConfig: { [key: string]: string }; 11 | 12 | constructor() { 13 | if (IS_DEV) { 14 | dotenv.config(findUp.sync('.env')); 15 | } 16 | this.envConfig = process.env; 17 | } 18 | 19 | getString(key: string): string { 20 | return this.envConfig[key]; 21 | } 22 | 23 | getNumber(key: string): number { 24 | return parseInt(this.envConfig[key], 10); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/api/global/config/db-config/db-config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 3 | import { ConfigService } from '../config.service'; 4 | import { DbConfigService } from './db-config.service'; 5 | 6 | describe('DbConfigService', () => { 7 | let service: DbConfigService; 8 | let configService: ConfigService; 9 | 10 | beforeEach(async () => { 11 | const module: TestingModule = await Test.createTestingModule({ 12 | providers: [ConfigService, 13 | DbConfigService], 14 | }).overrideProvider(ConfigService).useValue( 15 | { 16 | getString: () => 'FakeConfig', 17 | getNumber: () => 1234 18 | } 19 | ).compile(); 20 | 21 | configService = module.get(ConfigService); 22 | service = module.get(DbConfigService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | it('should return db config for postgres', () => { 29 | // jest.spyOn(configService, 'getString').mockImplementation(() => 'FakeConfig'); 30 | // jest.spyOn(configService, 'getNumber').mockImplementation(() => 1234); 31 | const dbConfig = service.createTypeOrmOptions() as PostgresConnectionOptions; 32 | expect(dbConfig.type).toBe('postgres'); 33 | expect(dbConfig.host).toBe('FakeConfig'); 34 | expect(dbConfig.port).toBe(1234); 35 | expect(dbConfig.username).toBe('FakeConfig'); 36 | expect(dbConfig.password).toBe('FakeConfig'); 37 | expect(dbConfig.database).toBe('FakeConfig'); 38 | expect(dbConfig.synchronize).toBeTruthy(); 39 | expect(dbConfig.password).toBe('FakeConfig'); 40 | }); 41 | it('should return db config for postgres and database with "NewConfig" for strings', () => { 42 | jest.spyOn(configService, 'getString').mockImplementation(() => 'NewConfig'); 43 | const dbConfig = service.createTypeOrmOptions() as PostgresConnectionOptions; 44 | expect(dbConfig.type).toBe('postgres'); 45 | expect(dbConfig.host).toBe('NewConfig'); 46 | expect(dbConfig.port).toBe(1234); 47 | expect(dbConfig.username).toBe('NewConfig'); 48 | expect(dbConfig.password).toBe('NewConfig'); 49 | expect(dbConfig.database).toBe('NewConfig'); 50 | expect(dbConfig.synchronize).toBeTruthy(); 51 | expect(dbConfig.password).toBe('NewConfig'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /server/src/api/global/config/db-config/db-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; 3 | import { ConfigService } from '../config.service'; 4 | 5 | @Injectable() 6 | export class DbConfigService implements TypeOrmOptionsFactory { 7 | constructor(private readonly config: ConfigService) { 8 | } 9 | 10 | createTypeOrmOptions(): TypeOrmModuleOptions { 11 | return { 12 | type: 'postgres', 13 | host: this.config.getString('DB_HOST'), 14 | port: this.config.getNumber('DB_PORT'), 15 | username: this.config.getString('DB_USERNAME'), 16 | password: this.config.getString('DB_PASSWORD'), 17 | database: this.config.getString('DB_DATABASE'), 18 | entities: [__dirname + '/**/*.entity{.ts,.js}'], 19 | synchronize: true, 20 | } as TypeOrmModuleOptions; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/api/global/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, HttpModule, Module } from '@nestjs/common'; 2 | import { Agent } from 'https'; 3 | import { ConfigService } from './config/config.service'; 4 | import { DbConfigService } from './config/db-config/db-config.service'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [ 9 | HttpModule.register({ 10 | timeout: 5000, 11 | maxRedirects: 5, 12 | httpsAgent: new Agent({ 13 | rejectUnauthorized: false 14 | }) 15 | }), 16 | ], 17 | providers: [ConfigService, DbConfigService], 18 | exports: [ConfigService, DbConfigService] 19 | }) 20 | export class GlobalModule { 21 | } 22 | -------------------------------------------------------------------------------- /server/src/api/global/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config/db-config/db-config.service'; 2 | export * from './config/config.service'; 3 | export * from './global.module'; 4 | -------------------------------------------------------------------------------- /server/src/api/user/controllers/v1.user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { V1UserController } from './v1.user.controller'; 3 | 4 | describe('User Controller', () => { 5 | let controller: V1UserController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [V1UserController], 10 | }).compile(); 11 | 12 | controller = module.get(V1UserController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /server/src/api/user/controllers/v1.user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { User } from '@shared/entities/user/user.entity'; 3 | import { Observable, of } from 'rxjs'; 4 | import { UserService } from '../services/user.service'; 5 | 6 | @Controller('v1/user') 7 | export class V1UserController { 8 | constructor(private readonly userService: UserService) { 9 | } 10 | 11 | @Get() 12 | getTasks(): Observable { 13 | return of(this.userService.getAlUsers()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/api/user/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import {UserService} from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /server/src/api/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from '../../../../../shared/entities/user/user.entity'; 3 | 4 | @Injectable() 5 | export class UserService { 6 | getAlUsers(): User[] { 7 | return [new User({defaultColor: 'somehitng', defaultFont: 'sm,ehtun', name: {first: 'first', last: 'last'}})]; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /server/src/api/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { V1UserController } from './controllers/v1.user.controller'; 3 | import { UserService } from './services/user.service'; 4 | 5 | @Module({ 6 | controllers: [V1UserController], 7 | providers: [UserService], 8 | }) 9 | export class UserModule { 10 | } 11 | -------------------------------------------------------------------------------- /server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {ApiModule} from './api/api.module'; 3 | import {SpaModule} from './spa/spaModule'; 4 | 5 | /** 6 | * This file should theoretically never change. 7 | * Thank you 8 | */ 9 | @Module({ 10 | imports: [ 11 | ApiModule, 12 | SpaModule, 13 | ], 14 | }) 15 | export class AppModule { 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { NestExpressApplication } from '@nestjs/platform-express'; 3 | import { join } from 'path'; 4 | import { SERVER_PORT } from '../config'; 5 | import { AppModule } from './app.module'; 6 | import { staticsRouter } from './statics-router'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule); 10 | 11 | app.setViewEngine('ejs'); 12 | 13 | app.useStaticAssets(join(__dirname, '../..', 'assets')); 14 | app.setBaseViewsDir(join(__dirname, '../..', 'views')); 15 | 16 | app.use(staticsRouter()); 17 | 18 | await app.listen(SERVER_PORT); 19 | } 20 | 21 | bootstrap(); 22 | -------------------------------------------------------------------------------- /server/src/spa/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { SpaController } from './spa.controller'; 3 | 4 | describe('SpaController', () => { 5 | let appController: SpaController; 6 | 7 | beforeEach(async () => { 8 | const app: TestingModule = await Test.createTestingModule({ 9 | controllers: [SpaController], 10 | providers: [], 11 | }).compile(); 12 | 13 | appController = app.get(SpaController); 14 | }); 15 | 16 | describe('root', () => { 17 | it('should be defined', () => { 18 | expect(appController).toBeDefined(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /server/src/spa/manifest-manager/manifest-manager.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ManifestManagerService } from './manifest-manager.service'; 3 | 4 | describe('ManifestManagerService', () => { 5 | let service: ManifestManagerService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [ManifestManagerService], 10 | }).compile(); 11 | 12 | service = module.get(ManifestManagerService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /server/src/spa/manifest-manager/manifest-manager.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService, Injectable } from '@nestjs/common'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { IS_DEV, WEBPACK_PORT } from '../../../config'; 5 | 6 | export interface IManifest { 7 | 'main.js': string; 8 | 'vendors.js': string; 9 | 'logo.png': string; 10 | } 11 | 12 | @Injectable() 13 | export class ManifestManagerService { 14 | constructor(private readonly http: HttpService) { 15 | } 16 | 17 | public async getManifest(): Promise { 18 | let manifest: IManifest; 19 | if (IS_DEV) { 20 | // load from webpack dev server 21 | manifest = await this.getManifestFromWebpack(); 22 | } else { 23 | // read from file system 24 | const manifestStr = fs.readFileSync(path.join(process.cwd(), 'dist', 'statics', 'manifest.json'), 'utf-8').toString(); 25 | manifest = JSON.parse(manifestStr); 26 | } 27 | return manifest; 28 | } 29 | 30 | private getManifestFromWebpack(): Promise { 31 | return new Promise((resolve, reject) => { 32 | this.http.get(`http://localhost:${WEBPACK_PORT}/statics/manifest.json`).subscribe( 33 | res => resolve(res.data as IManifest), 34 | error => reject(error) 35 | ); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/spa/spa.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Render } from '@nestjs/common'; 2 | import { ManifestManagerService } from './manifest-manager/manifest-manager.service'; 3 | 4 | @Controller() 5 | export class SpaController { 6 | constructor(private manifestManager: ManifestManagerService) { 7 | } 8 | 9 | @Get('**') 10 | @Render('page') 11 | async root() { 12 | const manifest = await this.manifestManager.getManifest(); 13 | return {manifest}; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/spa/spaModule.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { GlobalModule, DbConfigService } from '../api/global'; 4 | import { ManifestManagerService } from './manifest-manager/manifest-manager.service'; 5 | import { SpaController } from './spa.controller'; 6 | 7 | @Module({ 8 | imports: [ 9 | HttpModule, 10 | GlobalModule, 11 | TypeOrmModule.forRootAsync({ 12 | imports: [GlobalModule], 13 | useExisting: DbConfigService, 14 | }), 15 | ], 16 | controllers: [SpaController], 17 | providers: [ManifestManagerService], 18 | }) 19 | export class SpaModule { 20 | } 21 | -------------------------------------------------------------------------------- /server/src/statics-router.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Router } from 'express'; 3 | import * as path from 'path'; 4 | import { IS_DEV, WEBPACK_PORT } from '../config'; 5 | 6 | export function staticsRouter() { 7 | const router = Router(); 8 | 9 | if (IS_DEV) { 10 | const proxy = require('http-proxy-middleware'); 11 | // All the assets are hosted by Webpack on localhost:${config.WEBPACK_PORT} (Webpack-dev-server) 12 | router.use( 13 | '/statics', 14 | proxy({ 15 | target: `http://localhost:${WEBPACK_PORT}/`, 16 | }), 17 | ); 18 | } else { 19 | const staticsPath = path.join(process.cwd(), 'dist', 'statics'); 20 | 21 | // All the assets are in "statics" folder (Done by Webpack during the build phase) 22 | router.use('/statics', express.static(staticsPath)); 23 | } 24 | return router; 25 | } 26 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import * as request from 'supertest'; 3 | import { ApiModule } from '../src/api/api.module'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app; 7 | 8 | beforeEach(async () => { 9 | const moduleFixture: TestingModule = await Test.createTestingModule({ 10 | imports: [ApiModule], 11 | }).compile(); 12 | 13 | app = moduleFixture.createNestApplication(); 14 | await app.init(); 15 | }); 16 | 17 | it('/ (GET)', () => { 18 | return request(app.getHttpServer()) 19 | .get('/') 20 | .expect(200) 21 | .expect('Hello World!'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [ 4 | "node_modules", 5 | "test", 6 | "dist", 7 | "**/*spec.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "outDir": "../dist", 12 | "rootDir": "..", 13 | "lib": [ 14 | "es6" 15 | ], 16 | "moduleResolution": "node", 17 | "types": [ 18 | "node" 19 | ], 20 | "baseUrl": ".", 21 | "incremental": true, 22 | "paths": { 23 | "@shared/*": [ 24 | "../shared/*" 25 | ] 26 | } 27 | }, 28 | "files": [ 29 | "./src/main.ts" 30 | ], 31 | "exclude": [ 32 | "../node_modules" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /server/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es6", 9 | "dom" 10 | ], 11 | "jsx": "react", 12 | "types": [ 13 | "node", 14 | "jest" 15 | ], 16 | "noEmit": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shared/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giard-alexandre/fullstack-ts-react-nest/e9b051bef762faff1277ea9135ba0c405a877a7b/shared/.gitkeep -------------------------------------------------------------------------------- /shared/entities/entityBase.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from 'typeorm'; 2 | 3 | /** 4 | * Abstract class to define the base fields and columns for an Entity. 5 | * @param T The type of the child class inheriting from {@link BaseEntity} 6 | */ 7 | export abstract class EntityBase { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @CreateDateColumn() 12 | createdAt: Date; 13 | 14 | @UpdateDateColumn() 15 | updatedAt: Date; 16 | 17 | @VersionColumn() 18 | version: number; 19 | 20 | /** 21 | * @param {T} init The initialization parameters for the entity. 22 | * @returns {T} New instance of type {@link T}. 23 | */ 24 | constructor(init?: Partial) { 25 | Object.assign(this, init); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /shared/entities/partials/name.ts: -------------------------------------------------------------------------------- 1 | import { Column } from 'typeorm'; 2 | 3 | export class Name { 4 | @Column() 5 | first: string; 6 | 7 | @Column() 8 | last: string; 9 | } 10 | -------------------------------------------------------------------------------- /shared/entities/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { EntityBase } from '../entityBase'; 3 | import { Name } from '../partials/name'; 4 | 5 | @Entity() 6 | export class User extends EntityBase { 7 | 8 | @Column(type => Name) 9 | name: Name; 10 | 11 | @Column() 12 | defaultFont: string; 13 | 14 | @Column() 15 | defaultColor: string; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "sourceMap": true, 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "isolatedModules": true, 11 | "outDir": "./dist", 12 | "lib": [ 13 | "es2015", 14 | "dom" 15 | ], 16 | "jsx": "react", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@shared/*": [ 20 | "./shared/*" 21 | ] 22 | } 23 | }, 24 | "files": [ 25 | "./types.d.ts", 26 | "./client/client.tsx" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.webpack-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "eofline": true, 9 | "max-line-length": [ 10 | true, 11 | 150 12 | ], 13 | "quotemark": [ 14 | true, 15 | "single", 16 | "jsx-single" 17 | ], 18 | "arrow-parens": [ 19 | true, 20 | "ban-single-arg-parens" 21 | ], 22 | "trailing-comma": [ 23 | false, 24 | { 25 | "multiline": "always", 26 | "singleline": "always" 27 | } 28 | ], 29 | "ordered-imports": false, 30 | "object-literal-sort-keys": false, 31 | "interface-name": [ 32 | true 33 | ], 34 | "member-access": [ 35 | false 36 | ], 37 | "no-console": [ 38 | false, 39 | "log", 40 | "error" 41 | ], 42 | "no-var-requires": false 43 | }, 44 | "rulesDirectory": [] 45 | } 46 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare var require: (path: string) => any; 2 | declare var module: any; 3 | declare module '*.less'; 4 | declare module '*.svg'; 5 | -------------------------------------------------------------------------------- /views/page.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TypeScript and React 8 | 9 | 10 | 11 | 12 | 13 |
14 |
Loading...
15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | const cssnano = require('cssnano'); 2 | import * as fs from 'fs'; 3 | const OpenBrowserPlugin = require('open-browser-webpack-plugin'); 4 | import * as path from 'path'; 5 | import * as webpack from 'webpack'; 6 | // noinspection ES6UnusedImports 7 | const webpackDevServer = require('webpack-dev-server'); 8 | 9 | import { IS_DEV, SERVER_PORT, WEBPACK_PORT } from './server/config'; 10 | 11 | const ManifestPlugin = require('webpack-manifest-plugin'); 12 | 13 | // Fix for TsconfigPathsPlugin 14 | process.env.TS_NODE_PROJECT = ''; 15 | import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; 16 | 17 | const plugins = [new ManifestPlugin()]; 18 | 19 | const lessToJs = require('less-vars-to-js'); 20 | const themeVariables = lessToJs(fs.readFileSync(path.join(__dirname, './ant-theme-vars.less'), 'utf8')); 21 | 22 | // import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 23 | // plugins.push(new BundleAnalyzerPlugin()); 24 | 25 | if (IS_DEV) { 26 | plugins.push(new OpenBrowserPlugin({url: `http://localhost:${SERVER_PORT}`})); 27 | } 28 | 29 | const nodeModulesPath = path.resolve(__dirname, 'node_modules'); 30 | 31 | const config: webpack.Configuration = { 32 | mode: IS_DEV ? 'development' : 'production', 33 | devtool: IS_DEV ? 'inline-source-map' : false, 34 | entry: ['@babel/polyfill', './client/client'], 35 | output: { 36 | path: path.join(__dirname, 'dist', 'statics'), 37 | filename: `[name]-[hash:8]-bundle.js`, 38 | publicPath: '/statics/', 39 | }, 40 | resolve: { 41 | alias: {'react-dom': '@hot-loader/react-dom'}, 42 | extensions: ['.js', '.ts', '.tsx'], 43 | plugins: [ 44 | new TsconfigPathsPlugin({ configFile: path.join(__dirname, 'client', 'tsconfig.client.json') }) 45 | ] 46 | }, 47 | optimization: { 48 | splitChunks: { 49 | cacheGroups: { 50 | commons: { 51 | test: /[\\/]node_modules[\\/]/, 52 | name: 'vendors', 53 | chunks: 'all', 54 | }, 55 | }, 56 | }, 57 | }, 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.tsx?$/, 62 | loaders: ['babel-loader'], 63 | exclude: [/node_modules/, nodeModulesPath], 64 | }, 65 | { 66 | test: /\.less$/, 67 | exclude: /node_modules/, 68 | use: [ 69 | { 70 | loader: 'style-loader', 71 | }, 72 | { 73 | loader: 'css-loader', 74 | options: { 75 | modules: true, 76 | camelCase: true, 77 | sourceMap: IS_DEV, 78 | }, 79 | }, 80 | { 81 | loader: 'less-loader', 82 | options: { 83 | javascriptEnabled: true, 84 | // modifyVars: themeVariables, 85 | } 86 | }, 87 | { 88 | loader: 'postcss-loader', 89 | options: { 90 | sourceMap: IS_DEV, 91 | plugins: IS_DEV ? [cssnano()] : [], 92 | }, 93 | }, 94 | ], 95 | }, 96 | { 97 | test: /node_modules\/.*\.less$/, 98 | use: [ 99 | { 100 | loader: 'style-loader', 101 | }, 102 | { 103 | loader: 'css-loader', 104 | options: { 105 | sourceMap: true, 106 | }, 107 | }, 108 | { 109 | loader: 'less-loader', 110 | options: { 111 | javascriptEnabled: true, 112 | sourceMap: true, 113 | // modifyVars: themeVariables, 114 | // root: path.resolve(__dirname, './') 115 | } 116 | }, 117 | ] 118 | }, 119 | { 120 | test: /.jpe?g$|.gif$|.png$|.svg$|.woff$|.woff2$|.ttf$|.eot$/, 121 | use: 'url-loader?limit=10000', 122 | }, 123 | ], 124 | }, 125 | devServer: { 126 | port: WEBPACK_PORT, 127 | }, 128 | plugins, 129 | externals: {}, 130 | }; 131 | 132 | export default config; 133 | --------------------------------------------------------------------------------