├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── app ├── boostrap.ts ├── container │ └── index.ts ├── definitions.ts ├── error.ts ├── kernel │ ├── Container │ │ └── DependencyManager.ts │ ├── Exceptions │ │ ├── PersistenceErrors.ts │ │ ├── ValidationError.ts │ │ └── ValidationErrors.ts │ └── Routing │ │ ├── RouteDefinition.ts │ │ └── RouteManager.ts ├── main.ts ├── registerServiceWorker.ts ├── router │ └── index.ts ├── shims-vue.d.ts ├── store │ └── index.ts ├── ui │ ├── component.ts │ └── schema.ts └── util │ └── miscelaneous.ts ├── babel.config.js ├── cypress.json ├── diagram.drawio ├── infra └── http.ts ├── jest.config.js ├── package.json ├── presentation ├── assets │ └── logo.png ├── modules │ ├── Dashboard │ │ ├── DashboardLayout.vue │ │ ├── components │ │ │ ├── Button │ │ │ │ └── AppButton.vue │ │ │ ├── Container │ │ │ │ ├── AppField.vue │ │ │ │ └── AppForm.vue │ │ │ ├── Input │ │ │ │ ├── AppCheckbox.vue │ │ │ │ ├── AppText.vue │ │ │ │ ├── AppTextarea.vue │ │ │ │ └── helper.ts │ │ │ └── form.ts │ │ ├── controllers │ │ │ └── execute.ts │ │ ├── ui │ │ │ ├── loading.ts │ │ │ └── message.ts │ │ └── views │ │ │ └── category │ │ │ ├── CategoryForm.vue │ │ │ └── controller │ │ │ └── index.ts │ └── Welcome │ │ ├── WelcomeLayout.vue │ │ ├── components │ │ ├── HelloLink.vue │ │ └── HelloWorld.vue │ │ └── views │ │ ├── About.vue │ │ ├── Home.vue │ │ └── SignIn.vue ├── styles │ └── scss │ │ ├── __variables.scss │ │ ├── grid.scss │ │ └── main.scss └── views │ └── App.vue ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html └── robots.txt ├── routes ├── dashboard.ts └── welcome.ts ├── source ├── Domains │ ├── General │ │ └── Category │ │ │ ├── Adapters │ │ │ └── Http │ │ │ │ └── HttpCategoryRepository.ts │ │ │ ├── Contracts │ │ │ └── CategoryRepository.ts │ │ │ ├── Domain │ │ │ ├── Category.ts │ │ │ └── CategoryValidation.ts │ │ │ ├── Schema │ │ │ └── Category.ts │ │ │ └── UseCases │ │ │ └── AddCategory.ts │ └── Shared │ │ └── UseCase.ts └── Infra │ └── Adapters │ ├── Http │ ├── HttpRepository.ts │ └── helper.ts │ └── Repository.ts ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ └── example.spec.ts ├── tsconfig.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-strongly-recommended', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'no-useless-constructor': 'off', 18 | 'no-return-assign': 'off', 19 | 'no-prototype-builtins': 'off', 20 | 21 | '@typescript-eslint/no-empty-interface': 'off', 22 | '@typescript-eslint/no-var-requires': 'off', 23 | '@typescript-eslint/no-empty-function': 'off', 24 | '@typescript-eslint/ban-ts-comment': 'off' 25 | }, 26 | overrides: [ 27 | { 28 | files: [ 29 | '**/__tests__/*.{j,t}s?(x)', 30 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 31 | ], 32 | env: { 33 | jest: true 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 William Correa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-clean-arch 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | yarn test:unit 21 | ``` 22 | 23 | ### Run your end-to-end tests 24 | ``` 25 | yarn test:e2e 26 | ``` 27 | 28 | ### Lints and fixes files 29 | ``` 30 | yarn lint 31 | ``` 32 | 33 | ### Customize configuration 34 | See [Configuration Reference](https://cli.vuejs.org/config/). 35 | -------------------------------------------------------------------------------- /app/boostrap.ts: -------------------------------------------------------------------------------- 1 | import DependencyManager from './kernel/Container/DependencyManager' 2 | import http from 'infra/http' 3 | 4 | /** 5 | * @param {DependencyManager} container 6 | */ 7 | export default function (container: DependencyManager): void { 8 | container.set('http', http) 9 | 10 | container.addDefinition('CategoryValidation', () => import('source/Domains/General/Category/Domain/CategoryValidation')) 11 | container.addDefinition('CategoryRepository', () => import('source/Domains/General/Category/Adapters/Http/HttpCategoryRepository')) 12 | container.addDefinition('CategoryAddUseCase', () => import('source/Domains/General/Category/UseCases/AddCategory')) 13 | 14 | container.addDefinition('AddCategory', async function (container: DependencyManager): Promise { 15 | const AddCategory = await container.resolveDefinition('CategoryAddUseCase') 16 | const CategoryRepository = await container.resolveDefinition('CategoryRepository') 17 | const CategoryValidation = await container.resolveDefinition('CategoryValidation') 18 | // @ts-ignore 19 | return new AddCategory(new CategoryRepository(new CategoryValidation())) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /app/container/index.ts: -------------------------------------------------------------------------------- 1 | import DependencyManager from '../kernel/Container/DependencyManager' 2 | import { ContainerDefinition } from '../definitions' 3 | 4 | let container: DependencyManager 5 | 6 | /** 7 | * @param {string} property 8 | * @param {ContainerDefinition} value 9 | * @return {DependencyManager} 10 | */ 11 | export function set (property: string, value: ContainerDefinition): DependencyManager { 12 | return instance().set(property, value) 13 | } 14 | 15 | /** 16 | * @param {string} property 17 | * @return {unknown} 18 | */ 19 | export function get (property: string): unknown { 20 | return instance().get(property) 21 | } 22 | 23 | /** 24 | * @param {string} alias 25 | * @return {unknown} 26 | */ 27 | export function resolve (alias: string): unknown { 28 | return instance().resolveDefinition(alias) 29 | } 30 | 31 | /** 32 | * @return {DependencyManager} 33 | */ 34 | export function instance (): DependencyManager { 35 | if (!container) { 36 | container = new DependencyManager() 37 | } 38 | return container 39 | } 40 | -------------------------------------------------------------------------------- /app/definitions.ts: -------------------------------------------------------------------------------- 1 | import { Vue } from 'vue-class-component' 2 | 3 | /** 4 | * @typedef {UserEvent} 5 | */ 6 | export type UserEvent = Event & { 7 | target: T 8 | // probably you might want to add the currentTarget as well 9 | currentTarget: T 10 | } 11 | 12 | export interface ContainerForm extends Vue {} 13 | 14 | /** 15 | * @typedef {ContainerDefinition} 16 | */ 17 | export type ContainerDefinition = ((container: T) => Promise) | string | unknown 18 | 19 | /** 20 | * @typedef {ContainerProperty} 21 | */ 22 | export type ContainerProperty = ((container: T) => void) | unknown 23 | 24 | /** 25 | * @typedef {ErrorDetail} 26 | */ 27 | export type ErrorDetail = { 28 | message: string 29 | value?: unknown 30 | } 31 | 32 | /** 33 | * @typedef {ErrorScheme} 34 | */ 35 | export type ErrorScheme = Record 36 | 37 | /** 38 | * @typedef {Datum} 39 | */ 40 | export type Datum = Record 41 | 42 | /** 43 | * @typedef {Schema} 44 | */ 45 | export type Schema = { 46 | attrs: Record 47 | listeners: Record) => void)[]> 48 | on (event: string, handler: (event: Event | UserEvent) => void): Schema 49 | } 50 | 51 | /** 52 | * @typedef {Schemata} 53 | */ 54 | export type Schemata = Record 55 | 56 | /** 57 | * @typedef {HttpRequestConfig} 58 | */ 59 | export type HttpRequestConfig = Record 60 | 61 | /** 62 | * @typedef {HttpRestAnswer} 63 | */ 64 | export type HttpRestAnswer = { 65 | status: string 66 | value?: unknown 67 | meta?: unknown 68 | } 69 | 70 | /** 71 | * @typedef {HttpResponse} 72 | */ 73 | export interface HttpResponse { 74 | data: T 75 | status: number 76 | statusText: string 77 | headers: unknown 78 | config: HttpRequestConfig 79 | request?: unknown 80 | } 81 | 82 | /** 83 | * @typedef HttpClient 84 | */ 85 | export interface HttpClient { 86 | request> (config: HttpRequestConfig): Promise 87 | 88 | get> (url: string, config?: HttpRequestConfig): Promise 89 | 90 | delete> (url: string, config?: HttpRequestConfig): Promise 91 | 92 | head> (url: string, config?: HttpRequestConfig): Promise 93 | 94 | options> (url: string, config?: HttpRequestConfig): Promise 95 | 96 | post> (url: string, data?: unknown, config?: HttpRequestConfig): Promise 97 | 98 | put> (url: string, data?: unknown, config?: HttpRequestConfig): Promise 99 | 100 | patch> (url: string, data?: unknown, config?: HttpRequestConfig): Promise 101 | } 102 | -------------------------------------------------------------------------------- /app/error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Error} e 3 | */ 4 | export default function (e: Error): void { 5 | console.error(e) 6 | } 7 | -------------------------------------------------------------------------------- /app/kernel/Container/DependencyManager.ts: -------------------------------------------------------------------------------- 1 | import { ContainerDefinition, ContainerProperty } from '../../definitions' 2 | 3 | /** 4 | * @class {DependencyManager} 5 | */ 6 | export default class DependencyManager { 7 | /** 8 | * @type {Record} 9 | */ 10 | private definitions: Record> = {} 11 | 12 | /** 13 | * @type {Record} 14 | */ 15 | private properties: Record = {} 16 | 17 | /** 18 | * @return {DependencyManager} 19 | */ 20 | static create (): DependencyManager { 21 | return new this() 22 | } 23 | 24 | /** 25 | * @param {string} property 26 | * @param {(container: Container) => void | unknown} value 27 | * @return this 28 | */ 29 | set (property: string, value: ContainerProperty): this { 30 | this.properties[property] = value 31 | return this 32 | } 33 | 34 | /** 35 | * @param {string} property 36 | * @return {unknown} 37 | */ 38 | get (property: string): unknown { 39 | return this.properties[property] 40 | } 41 | 42 | /** 43 | * @param {string} alias 44 | * @param {string} target 45 | * @return this 46 | */ 47 | addDefinition (alias: string, target: ContainerDefinition): this { 48 | this.definitions[alias] = target 49 | return this 50 | } 51 | 52 | /** 53 | * @param {Record} definitions 54 | * @return this 55 | */ 56 | addDefinitions (definitions: Record): this { 57 | Object.assign(this.definitions, definitions) 58 | return this 59 | } 60 | 61 | /** 62 | * @param {string} alias 63 | * @return {unknown} 64 | */ 65 | async resolveDefinition (alias: string): Promise { 66 | const definition = this.definitions[alias] 67 | if (!definition) { 68 | return null 69 | } 70 | if (typeof definition !== 'function') { 71 | return definition 72 | } 73 | const reference = await definition(this) 74 | if (reference.default) { 75 | return reference.default 76 | } 77 | return reference 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/kernel/Exceptions/PersistenceErrors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorScheme } from '../../definitions' 2 | 3 | /** 4 | * @class {PersistenceErrors} 5 | */ 6 | export default class PersistenceErrors extends Error { 7 | /** 8 | * @type {ErrorScheme} 9 | */ 10 | public readonly errors: ErrorScheme = {} 11 | 12 | /** 13 | * @param {ErrorScheme} errors 14 | */ 15 | constructor (errors: ErrorScheme) { 16 | super('PersistenceErrors') 17 | 18 | this.errors = errors 19 | } 20 | 21 | /** 22 | * @return {ErrorScheme} 23 | */ 24 | getErrors (): ErrorScheme { 25 | return this.errors 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/kernel/Exceptions/ValidationError.ts: -------------------------------------------------------------------------------- 1 | import { ErrorDetail } from '../../definitions' 2 | 3 | /** 4 | * @class {ValidationError} 5 | */ 6 | export default class ValidationError extends Error { 7 | /** 8 | * @type {Record} 9 | */ 10 | public readonly errors: Record = {} 11 | 12 | /** 13 | * @param {string} field 14 | * @param {string} message 15 | * @param {unknown} value 16 | */ 17 | constructor (field: string, message: string, value?: unknown) { 18 | super('ValidationError') 19 | 20 | this.errors[field] = [ 21 | { 22 | message, 23 | value 24 | } 25 | ] 26 | } 27 | 28 | /** 29 | * @return {Record} 30 | */ 31 | getErrors (): Record { 32 | return this.errors 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/kernel/Exceptions/ValidationErrors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorScheme } from '../../definitions' 2 | 3 | /** 4 | * @class {ValidationErrors} 5 | */ 6 | export default class ValidationErrors extends Error { 7 | /** 8 | * @type {ErrorScheme} 9 | */ 10 | public readonly errors: ErrorScheme = {} 11 | 12 | /** 13 | * @param {ErrorScheme} errors 14 | */ 15 | constructor (errors: ErrorScheme) { 16 | super('ValidationErrors') 17 | 18 | this.errors = errors 19 | } 20 | 21 | /** 22 | * @return {ErrorScheme} 23 | */ 24 | getErrors (): ErrorScheme { 25 | return this.errors 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/kernel/Routing/RouteDefinition.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NavigationGuardWithThis, 3 | RouteComponent, 4 | RouteMeta, 5 | RouteRecordName, 6 | RouteRecordRaw, 7 | RouteRecordRedirectOption 8 | } from 'vue-router' 9 | import RouteManager from './RouteManager' 10 | 11 | /** 12 | */ 13 | export default class RouteDefinition { 14 | /** 15 | * Path of the record. Should start with `/` unless the record is the child of 16 | * another record. 17 | * 18 | * @example `/users/:id` matches `/users/1` as well as `/users/username`. 19 | */ 20 | path: string 21 | 22 | /** 23 | * Component to display when the URL matches this route. 24 | */ 25 | component: RouteComponent 26 | 27 | /** 28 | * Where to redirect if the route is directly matched. The redirection happens 29 | * before any navigation guard and triggers a new navigation with the new 30 | * target location. 31 | */ 32 | redirect?: RouteRecordRedirectOption 33 | 34 | /** 35 | * Array of nested routes. 36 | */ 37 | children?: RouteRecordRaw[] 38 | 39 | /** 40 | * Aliases for the record. Allows defining extra paths that will behave like a 41 | * copy of the record. Allows having paths shorthands like `/users/:id` and 42 | * `/u/:id`. All `alias` and `path` values must share the same params. 43 | */ 44 | alias?: string | string[] 45 | 46 | /** 47 | * Name for the route record. 48 | */ 49 | name?: RouteRecordName 50 | 51 | /** 52 | * Before Enter guard specific to this record. Note `beforeEnter` has no 53 | * effect if the record has a `redirect` property. 54 | */ 55 | beforeEnter?: NavigationGuardWithThis | NavigationGuardWithThis[] 56 | 57 | /** 58 | * Arbitrary data attached to the record. 59 | */ 60 | meta?: RouteMeta 61 | 62 | /** 63 | * @param {Record} options 64 | */ 65 | constructor (options: Record) { 66 | this.path = options.path as string 67 | this.component = options.component as RouteComponent 68 | if (options.children) { 69 | this.children = options.children as RouteRecordRaw[] 70 | } 71 | 72 | if (options.redirect) { 73 | this.redirect = options.redirect as RouteRecordRedirectOption 74 | } 75 | if (options.children) { 76 | this.children = options.children as RouteRecordRaw[] 77 | } 78 | if (options.alias) { 79 | this.alias = options.alias as string | string[] 80 | } 81 | if (options.name) { 82 | this.name = options.name as RouteRecordName 83 | } 84 | if (options.beforeEnter) { 85 | this.beforeEnter = options.beforeEnter as NavigationGuardWithThis | NavigationGuardWithThis[] 86 | } 87 | if (options.meta) { 88 | this.meta = options.meta as RouteMeta 89 | } 90 | } 91 | 92 | /** 93 | * @param {(callback: (router: RouteManager) => void} callback 94 | * @return {this} 95 | */ 96 | addChildren (callback: (router: RouteManager) => void): RouteDefinition { 97 | const router = RouteManager.build() 98 | callback(router) 99 | const children = router.getRoutes() 100 | this.setChildren(children) 101 | return this 102 | } 103 | 104 | /** 105 | * @return {string} 106 | */ 107 | getPath (): string { 108 | return this.path 109 | } 110 | 111 | /** 112 | * @param {string} path 113 | * @return {this} 114 | */ 115 | setPath (path: string): RouteDefinition { 116 | this.path = path 117 | return this 118 | } 119 | 120 | /** 121 | * @return {RouteComponent} 122 | */ 123 | getComponent (): RouteComponent { 124 | return this.component 125 | } 126 | 127 | /** 128 | * @param {RouteComponent} component 129 | * @return {this} 130 | */ 131 | setComponent (component: RouteComponent): RouteDefinition { 132 | this.component = component 133 | return this 134 | } 135 | 136 | /** 137 | * @return {RouteRecordRedirectOption | undefined} 138 | */ 139 | getRedirect (): RouteRecordRedirectOption | undefined { 140 | return this.redirect 141 | } 142 | 143 | /** 144 | * @param {RouteRecordRedirectOption} redirect 145 | * @return {this} 146 | */ 147 | setRedirect (redirect: RouteRecordRedirectOption): RouteDefinition { 148 | this.redirect = redirect 149 | return this 150 | } 151 | 152 | /** 153 | * @return {RouteRecordRaw[] | undefined} 154 | */ 155 | getChildren (): RouteRecordRaw[] | undefined { 156 | return this.children 157 | } 158 | 159 | /** 160 | * @param {RouteRecordRaw} children 161 | * @return {this} 162 | */ 163 | setChildren (children: RouteRecordRaw[]): RouteDefinition { 164 | this.children = children 165 | return this 166 | } 167 | 168 | /** 169 | * @return {string | string[] | undefined} 170 | */ 171 | getAlias (): string | string[] | undefined { 172 | return this.alias 173 | } 174 | 175 | /** 176 | * @param {string} alias 177 | * @return {this} 178 | */ 179 | setAlias (alias: string | string[]): RouteDefinition { 180 | this.alias = alias 181 | return this 182 | } 183 | 184 | /** 185 | * @return {RouteRecordName | undefined} 186 | */ 187 | getName (): RouteRecordName | undefined { 188 | return this.name 189 | } 190 | 191 | /** 192 | * @param {RouteRecordName} name 193 | * @return {this} 194 | */ 195 | setName (name: RouteRecordName): RouteDefinition { 196 | this.name = name 197 | return this 198 | } 199 | 200 | /** 201 | * @return {NavigationGuardWithThis | NavigationGuardWithThis[] | undefined} 202 | */ 203 | getBeforeEnter (): NavigationGuardWithThis | NavigationGuardWithThis[] | undefined { 204 | return this.beforeEnter 205 | } 206 | 207 | /** 208 | * @param {NavigationGuardWithThis} beforeEnter 209 | * @return {this} 210 | */ 211 | setBeforeEnter (beforeEnter: NavigationGuardWithThis | NavigationGuardWithThis[]): RouteDefinition { 212 | this.beforeEnter = beforeEnter 213 | return this 214 | } 215 | 216 | /** 217 | * @return {RouteMeta | undefined} 218 | */ 219 | getMeta (): RouteMeta | undefined { 220 | return this.meta 221 | } 222 | 223 | /** 224 | * @param {RouteMeta} meta 225 | * @return {this} 226 | */ 227 | setMeta (meta: RouteMeta): RouteDefinition { 228 | this.meta = meta 229 | return this 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /app/kernel/Routing/RouteManager.ts: -------------------------------------------------------------------------------- 1 | import { RouteComponent, RouteRecordRaw } from 'vue-router' 2 | import RouteDefinition from './RouteDefinition' 3 | 4 | /** 5 | * @class {RouteManager} 6 | */ 7 | export default class RouteManager { 8 | private routes: RouteRecordRaw[] = [] 9 | 10 | /** 11 | * @return {RouteManager} 12 | */ 13 | static build (): RouteManager { 14 | return new this() 15 | } 16 | 17 | /** 18 | * @param {string} path 19 | * @param {RouteComponent} component 20 | * @return {RouteDefinition} 21 | */ 22 | on (path: string, component: RouteComponent): RouteDefinition { 23 | const options = { 24 | path, 25 | component 26 | } 27 | const route = new RouteDefinition(options) 28 | this.routes.push(route) 29 | return route 30 | } 31 | 32 | /** 33 | * @param {string} path 34 | * @param {RouteComponent} component 35 | * @return {RouteDefinition} 36 | */ 37 | route (path: string, component: RouteComponent): RouteDefinition { 38 | return this.on(path, component) 39 | } 40 | 41 | /** 42 | * @param {string} path 43 | * @param {RouteComponent} component 44 | * @param {(router: Router) => void} callback 45 | * @return {RouteDefinition} 46 | */ 47 | group (path: string, component: RouteComponent, callback: (router: RouteManager) => void): RouteDefinition { 48 | const router = RouteManager.build() 49 | callback(router) 50 | const children = router.getRoutes() 51 | const options = { 52 | path, 53 | component, 54 | children 55 | } 56 | const route = new RouteDefinition(options) 57 | this.routes.push(route) 58 | return route 59 | } 60 | 61 | /** 62 | * @param {Record} source 63 | * @return {RouteManager} 64 | */ 65 | register (source: Record): RouteManager { 66 | this.routes.push(new RouteDefinition(source)) 67 | return this 68 | } 69 | 70 | /** 71 | * @return {RouteRecordRaw[]} 72 | */ 73 | getRoutes (): RouteRecordRaw[] { 74 | return this.routes 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import router from './router' 3 | import store from './store' 4 | 5 | import App from 'presentation/views/App.vue' 6 | import 'presentation/styles/scss/main.scss' 7 | 8 | import boostrap from './boostrap' 9 | import { instance } from './container' 10 | 11 | import './registerServiceWorker' 12 | 13 | boostrap(instance()) 14 | 15 | const app = createApp(App) 16 | 17 | app.use(store) 18 | .use(router) 19 | .mount('#app') 20 | 21 | export default app 22 | -------------------------------------------------------------------------------- /app/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from 'register-service-worker' 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready () { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB' 11 | ) 12 | }, 13 | registered () { 14 | console.log('Service worker has been registered.') 15 | }, 16 | cached () { 17 | console.log('Content has been cached for offline use.') 18 | }, 19 | updatefound () { 20 | console.log('New content is downloading.') 21 | }, 22 | updated () { 23 | console.log('New content is available; please refresh.') 24 | }, 25 | offline () { 26 | console.log('No internet connection found. App is running in offline mode.') 27 | }, 28 | error (error) { 29 | console.error('Error during service worker registration:', error) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /app/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | import RouteManager from 'app/kernel/Routing/RouteManager' 4 | 5 | import welcome from 'routes/welcome' 6 | import dashboard from 'routes/dashboard' 7 | 8 | const router = RouteManager.build() 9 | 10 | welcome(router) 11 | dashboard(router) 12 | 13 | const vueRouter = createRouter({ 14 | history: createWebHistory(), 15 | routes: router.getRoutes() 16 | }) 17 | 18 | export default vueRouter 19 | -------------------------------------------------------------------------------- /app/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | 3 | export default createStore({ 4 | state: { 5 | }, 6 | mutations: { 7 | }, 8 | actions: { 9 | }, 10 | modules: { 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /app/ui/component.ts: -------------------------------------------------------------------------------- 1 | import app from 'app/main' 2 | import { Component } from '@vue/runtime-core' 3 | 4 | const registered: Record = {} 5 | 6 | /** 7 | * @param {string} name 8 | * @param {Component} component 9 | */ 10 | export function register (name: string, component: Component): void { 11 | if (registered[name]) { 12 | return 13 | } 14 | registered[name] = true 15 | 16 | app.component(name, component) 17 | } 18 | -------------------------------------------------------------------------------- /app/ui/schema.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import { Schema, UserEvent } from '../definitions' 3 | 4 | /** 5 | * @param {Record} attrs 6 | * @return {Schema} 7 | */ 8 | export function create (attrs: Record): Schema { 9 | const listeners: Record) => void)[]> = {} 10 | const schema = { 11 | attrs, 12 | listeners, 13 | errors: [], 14 | on (event: string, handler: (event: Event | UserEvent) => void): Schema { 15 | if (!listeners[event]) { 16 | listeners[event] = [] 17 | } 18 | listeners[event].push(handler) 19 | return this 20 | } 21 | } 22 | return reactive(schema) 23 | } 24 | 25 | /** 26 | * @param {Record} attrs 27 | * @return {Schema} 28 | */ 29 | export function createText (attrs: Record): Schema { 30 | return create({ ...attrs, as: 'text' }) 31 | } 32 | 33 | /** 34 | * @param {Record} attrs 35 | * @return {Schema} 36 | */ 37 | export function createCheckbox (attrs: Record): Schema { 38 | return create({ ...attrs, as: 'checkbox' }) 39 | } 40 | 41 | /** 42 | * @param {Record} attrs 43 | * @return {Schema} 44 | */ 45 | export function createTextarea (attrs: Record): Schema { 46 | return create({ ...attrs, as: 'textarea' }) 47 | } 48 | 49 | /** 50 | * @param {Record} payload 51 | * @return {Record} 52 | */ 53 | export const observable = (payload: Record): Record => reactive(payload) 54 | -------------------------------------------------------------------------------- /app/util/miscelaneous.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string=} [prefix=''] 3 | * @param {boolean=} [moreEntropy=false] 4 | * @returns {string} 5 | */ 6 | export function id (prefix = '', moreEntropy = false): string { 7 | // discuss at: http://locutus.io/php/uniqid/ 8 | // original by: Kevin van Zonneveld (http://kvz.io) 9 | // revised by: Kankrelune (http://www.webfaktory.info/) 10 | // note 1: Uses an internal counter (in locutus global) to avoid collision 11 | // example 1: var $id = uniqid() 12 | // example 1: var $result = $id.length === 13 13 | // returns 1: true 14 | // example 2: var $id = uniqid('foo') 15 | // example 2: var $result = $id.length === (13 + 'foo'.length) 16 | // returns 2: true 17 | // example 3: var $id = uniqid('bar', true) 18 | // example 3: var $result = $id.length === (23 + 'bar'.length) 19 | // returns 3: true 20 | 21 | if (typeof prefix === 'undefined') { 22 | prefix = '' 23 | } 24 | 25 | let retId 26 | const _formatSeed = function (seed: string, reqWidth: number) { 27 | seed = parseInt(seed, 10).toString(16) // to hex str 28 | if (reqWidth < seed.length) { 29 | // so long we split 30 | return seed.slice(seed.length - reqWidth) 31 | } 32 | if (reqWidth > seed.length) { 33 | // so short we pad 34 | return Array(1 + (reqWidth - seed.length)).join('0') + seed 35 | } 36 | return seed 37 | } 38 | 39 | // @ts-ignore 40 | const $global = (typeof window !== 'undefined' ? window : global) 41 | $global.$locutus = $global.$locutus || {} 42 | const $locutus = $global.$locutus 43 | $locutus.php = $locutus.php || {} 44 | 45 | if (!$locutus.php.uniqidSeed) { 46 | // init seed with big random int 47 | $locutus.php.uniqidSeed = Math.floor(Math.random() * 0x75bcd15) 48 | } 49 | $locutus.php.uniqidSeed++ 50 | 51 | // start with prefix, add current milliseconds hex string 52 | retId = prefix 53 | retId += _formatSeed(String(parseInt(String(new Date().getTime() / 1000), 10)), 8) 54 | // add seed hex string 55 | retId += _formatSeed($locutus.php.uniqidSeed, 5) 56 | if (moreEntropy) { 57 | // for more entropy we add a float lower to 10 58 | retId += (Math.random() * 10).toFixed(8).toString() 59 | } 60 | 61 | return retId 62 | } 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /diagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /infra/http.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export default axios.create({ baseURL: 'https://webhook.site' }) 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-clean-arch", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "test:e2e": "vue-cli-service test:e2e", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.1", 15 | "bulma": "^0.9.2", 16 | "core-js": "^3.6.5", 17 | "register-service-worker": "^1.7.1", 18 | "vue": "^3.0.0", 19 | "vue-class-component": "^8.0.0-0", 20 | "vue-router": "^4.0.0-0", 21 | "vuex": "^4.0.0-0" 22 | }, 23 | "devDependencies": { 24 | "@types/jest": "^24.0.19", 25 | "@typescript-eslint/eslint-plugin": "^4.18.0", 26 | "@typescript-eslint/parser": "^4.18.0", 27 | "@vue/cli-plugin-babel": "~4.5.0", 28 | "@vue/cli-plugin-e2e-cypress": "~4.5.0", 29 | "@vue/cli-plugin-eslint": "~4.5.0", 30 | "@vue/cli-plugin-pwa": "~4.5.0", 31 | "@vue/cli-plugin-router": "~4.5.0", 32 | "@vue/cli-plugin-typescript": "~4.5.0", 33 | "@vue/cli-plugin-unit-jest": "~4.5.0", 34 | "@vue/cli-plugin-vuex": "~4.5.0", 35 | "@vue/cli-service": "~4.5.0", 36 | "@vue/compiler-sfc": "^3.0.0", 37 | "@vue/eslint-config-standard": "^5.1.2", 38 | "@vue/eslint-config-typescript": "^7.0.0", 39 | "@vue/test-utils": "^2.0.0-0", 40 | "eslint": "^6.7.2", 41 | "eslint-plugin-import": "^2.20.2", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-promise": "^4.2.1", 44 | "eslint-plugin-standard": "^4.0.0", 45 | "eslint-plugin-vue": "^7.0.0", 46 | "node-sass": "^4.12.0", 47 | "sass-loader": "^8.0.2", 48 | "typescript": "~4.1.5", 49 | "vue-jest": "^5.0.0-0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /presentation/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilcorrea/vuejs-clean-arch/7b4c1cf50dace514b4c74dbc596ff5cd6840fd3a/presentation/assets/logo.png -------------------------------------------------------------------------------- /presentation/modules/Dashboard/DashboardLayout.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 45 | 46 | 49 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Button/AppButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Container/AppField.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Container/AppForm.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 21 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Input/AppCheckbox.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 63 | 64 | 72 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Input/AppText.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 67 | 68 | 79 | -------------------------------------------------------------------------------- /presentation/modules/Dashboard/components/Input/AppTextarea.vue: -------------------------------------------------------------------------------- 1 |