From 5144e681bd9eee136034f1abc76fc8e22eae106c Mon Sep 17 00:00:00 2001 From: Joanna May Date: Wed, 24 Jul 2024 10:05:26 -0500 Subject: [PATCH 1/2] chore: markdown lint, link check, fix lint errors & update emojis --- .github/workflows/deploy.yml | 2 +- .github/workflows/markdown_links.yml | 12 ++ .github/workflows/markdown_lint.yml | 17 ++ .mdlintrc | 7 + .vscode/extensions.json | 1 + .vscode/settings.json | 3 + cspell.json | 17 +- package-lock.json | 194 ++++++++++++++++++ package.json | 2 + src/content/docs/architecture/backend.md | 53 +++-- src/content/docs/architecture/index.md | 8 +- src/content/docs/code_style/code_style.md | 8 +- src/content/docs/development/philosophy.mdx | 2 +- .../state_management/event_transformers.md | 67 +++--- .../docs/testing/golden_file_testing.md | 2 +- src/content/docs/testing/testing.md | 4 +- src/content/docs/theming/theming.md | 11 +- 17 files changed, 338 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/markdown_links.yml create mode 100644 .github/workflows/markdown_lint.yml create mode 100644 .mdlintrc diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9318367..6f7cf44 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy to GitHub Pages +name: ๐ŸŒŽ Deploy to GitHub Pages on: # Trigger the workflow every time you push to the `main` branch diff --git a/.github/workflows/markdown_links.yml b/.github/workflows/markdown_links.yml new file mode 100644 index 0000000..5dc991e --- /dev/null +++ b/.github/workflows/markdown_links.yml @@ -0,0 +1,12 @@ +name: ๐Ÿ”— Markdown Link Check + +on: push + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + folder-path: src/ diff --git a/.github/workflows/markdown_lint.yml b/.github/workflows/markdown_lint.yml new file mode 100644 index 0000000..ebe6bb5 --- /dev/null +++ b/.github/workflows/markdown_lint.yml @@ -0,0 +1,17 @@ +name: ๐Ÿ‘€ Markdown Lint Check + +on: push + +jobs: + markdown-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout your repository using git + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - run: npm run lint diff --git a/.mdlintrc b/.mdlintrc new file mode 100644 index 0000000..9a77e67 --- /dev/null +++ b/.mdlintrc @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/v0.34.0/schema/markdownlint-config-schema.json", + "default": true, + "no-inline-html": false, + "line-length": false, + "no-duplicate-heading": false +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f322eb6..ce05399 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,7 @@ "recommendations": [ "astro-build.astro-vscode", "bradlc.vscode-tailwindcss", + "DavidAnson.vscode-markdownlint", "unifiedjs.vscode-mdx" ], "unwantedRecommendations": [] diff --git a/.vscode/settings.json b/.vscode/settings.json index 1883be0..d9986ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,9 @@ "**/*.tsx", "**/*.json" ], + "markdownlint.config": { + "MD024": false + }, "tailwindCSS.includeLanguages": { "plaintext": "html" } diff --git a/cspell.json b/cspell.json index b640e72..e7e68b8 100644 --- a/cspell.json +++ b/cspell.json @@ -1,12 +1,25 @@ { - "ignorePaths": ["node_modules/**", ".**/"], - "files": ["**/*.md", "**/*.mdx", "**/*.tsx", "**/*.ts", "**/*.json"], + "ignorePaths": [ + "node_modules/**", + ".**/" + ], + "files": [ + "**/*.md", + "**/*.mdx", + "**/*.tsx", + "**/*.ts", + "**/*.json" + ], "words": [ "astro", "astrojs", + "Cupertino", "fontsource", "incentivized", + "laboratoria", + "mdlint", "multiplatform", + "pubspec", "tailwindcss", "todos", "tsconfigs" diff --git a/package-lock.json b/package-lock.json index 99499bb..3dfaa09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "typescript": "^5.4.5" }, "devDependencies": { + "@laboratoria/mdlint": "^1.2.3", "eslint": "^9.3.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-astro": "^1.2.0", @@ -2072,6 +2073,115 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@laboratoria/mdlint": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@laboratoria/mdlint/-/mdlint-1.2.3.tgz", + "integrity": "sha512-nAMW62fOHTl6tA8oyR3cjdEhMhppxq0BIp3S/KHiRBr2LOvrWu9oFnRCxXhd/0YHYlU1a9jeIDpGAscXoYaJ+g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "glob": "^7.2.0", + "markdownlint": "^0.25.1", + "minimist": "^1.2.5" + }, + "bin": { + "mdlint": "index.js" + }, + "engines": { + "node": ">=12.x" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@laboratoria/mdlint/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@laboratoria/mdlint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.0.1.tgz", @@ -5220,6 +5330,12 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -5853,6 +5969,17 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6266,6 +6393,15 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/load-plugin": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/load-plugin/-/load-plugin-6.0.3.tgz", @@ -6418,6 +6554,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -6427,6 +6588,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdownlint": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", + "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "dev": true, + "dependencies": { + "markdown-it": "12.3.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -6742,6 +6915,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -8019,6 +8198,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9799,6 +9987,12 @@ "node": ">=10" } }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, "node_modules/ultrahtml": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", diff --git a/package.json b/package.json index b6f18b0..115acb9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "astro preview", "astro": "astro", "type:check": "tsc", + "lint": "mdlint ./src/", "format": "prettier --write . --ext js,json,jsonc,ts,tsx,jsx,md,mdx", "format:check": "prettier --check ." }, @@ -30,6 +31,7 @@ "typescript": "^5.4.5" }, "devDependencies": { + "@laboratoria/mdlint": "^1.2.3", "eslint": "^9.3.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-astro": "^1.2.0", diff --git a/src/content/docs/architecture/backend.md b/src/content/docs/architecture/backend.md index 6911aea..35f0ba4 100644 --- a/src/content/docs/architecture/backend.md +++ b/src/content/docs/architecture/backend.md @@ -1,6 +1,6 @@ --- -title: Backend Architecture -description: Best practices for building backend APIs. +title: ๐Ÿ—„๏ธ Backend Architecture +description: Best practices for building backend APIs. --- Loose coupling, separation of concerns and layered architecture should not only be applied to frontend development. These principles can also be applied during backend development. For example, concepts such as route navigation, data access, data processing and data models can be separated and tested in isolation. @@ -8,12 +8,12 @@ Loose coupling, separation of concerns and layered architecture should not only :::note There are many languages and frameworks to write backends in. It is important to choose tools and follow patterns that serve business needs and maximize developer efficiency. VGV built [Dart Frog](https://dartfrog.vgv.dev/) for this purpose. Dart Frog provides a number of advantages to developers writing Flutter apps: - * Writing Dart code in both backend and frontend limits developer context switching and allows model reuse throughout the project - * Dart Frog's minimalistic design allows for flexibility and customization to suit individual app needs - * [Providers](https://dartfrog.vgv.dev/docs/basics/dependency-injection) and [middleware](https://dartfrog.vgv.dev/docs/basics/middleware) allow for easy dependency injection - * File-based [routing](https://dartfrog.vgv.dev/docs/basics/routes) makes endpoint creation simple - * All backend code is [easily testable](https://dartfrog.vgv.dev/docs/basics/testing) - * Access to features such as Dart dev tools and hot reload speed development time +- Writing Dart code in both backend and frontend limits developer context switching and allows model reuse throughout the project +- Dart Frog's minimalistic design allows for flexibility and customization to suit individual app needs +- [Providers](https://dartfrog.vgv.dev/docs/basics/dependency-injection) and [middleware](https://dartfrog.vgv.dev/docs/basics/middleware) allow for easy dependency injection +- File-based [routing](https://dartfrog.vgv.dev/docs/basics/routes) makes endpoint creation simple +- All backend code is [easily testable](https://dartfrog.vgv.dev/docs/basics/testing) +- Access to features such as Dart dev tools and hot reload speed development time ::: @@ -21,14 +21,13 @@ There are many languages and frameworks to write backends in. It is important to Putting the backend in the same repository as the frontend allows developers to easily import data models from the backend. Within the backend directory, developers should consider separating the following elements into dedicated directories: -* Middleware providers -* Routes -* Data access -* Data models -* Tests - -While providers, routes, and tests, can live in the root backend project, consider putting data models and data access into their own dedicated package(s). Ideally, these layers should be able to exist independently from the rest of the app. +- Middleware providers +- Routes +- Data access +- Data models +- Tests +While providers, routes, and tests, can live in the root backend project, consider putting data models and data access into their own dedicated package(s). Ideally, these layers should be able to exist independently from the rest of the app. ```txt my_app/ @@ -46,7 +45,7 @@ my_app/ | | | | |- src/ | | | | | |- endpoint_models/ | | | | | |- shared_models/ - | | |- data_source/ + | | |- data_source/ | | | |- lib/ | | | | |- src/ | | | |- test/ @@ -57,7 +56,7 @@ my_app/ | | | | |- todos/ | | |- test/ | | | |- src/ - | | | | |- middleware/ + | | | | |- middleware/ | | | |- routes/ | | | | |- api/ | | | | | |- v1/ @@ -81,13 +80,14 @@ final class GetTodosResponse { final List todos; } ``` + :::note -It is also advisable to automate JSON serialization inside the models package. This can be achieved with the [json_serializable](https://pub.dev/packages/json_serializable) package, though experimental [macros](https://dart.dev/language/macros) in Dart offer a potentially cleaner way of doing this in the future. +It is also advisable to automate JSON serialization inside the models package. This can be achieved with the [json_serializable](https://pub.dev/packages/json_serializable) package, though experimental [macros](https://dart.dev/language/macros) in Dart offer a potentially cleaner way of doing this in the future. ::: ### Data Access -A data source package should allow developers to fetch data from different sources. Similar to the data layer on the frontend, this package should abstract the work of fetching data and providing it to the API routes. This allows for easy development by mocking data in an in-memory source when necessary, or also creating different data sources for different environments. +A data source package should allow developers to fetch data from different sources. Similar to the data layer on the frontend, this package should abstract the work of fetching data and providing it to the API routes. This allows for easy development by mocking data in an in-memory source when necessary, or also creating different data sources for different environments. The best way to achieve this is by making an abstract data source with the necessary CRUD methods, and implementing this data source as needed based on where the data is coming from. @@ -113,29 +113,27 @@ Routes should follow common [best practices for REST API design](https://swagger #### Endpoints Should Have Descriptive Paths -Endpoints should be named for the collection of objects that they provide access to. Use plural nouns to specify the collection, not the individual entity. - +Endpoints should be named for the collection of objects that they provide access to. Use plural nouns to specify the collection, not the individual entity. ```txt my_api/v1/todos ``` -Nested paths then provide specific data about an individual object within the collection. +Nested paths then provide specific data about an individual object within the collection. ```txt my_api/v1/todos/1 ``` -When setting up a collection of objects that is nested under another collection, the endpoint path should reflect the relationship. +When setting up a collection of objects that is nested under another collection, the endpoint path should reflect the relationship. ```txt my_api/v1/users/123/todos ``` - #### Use Query Parameters to Filter Properties of GET results -Query parameters serve as the standard way of filtering the results of a GET request. +Query parameters serve as the standard way of filtering the results of a GET request. ```txt my_api/v1/todos?completed=false @@ -154,7 +152,7 @@ extension RequestBodyDecoder on Request { The request body can then be converted into the correct data model like in the endpoint code. -```dart +```dart final body = CreateTodoRequest.fromJson(await context.request.map()); ``` @@ -162,7 +160,6 @@ final body = CreateTodoRequest.fromJson(await context.request.map()); For update requests, PATCH is more advisable than PUT because [PATCH requests the server to update an existing entity, while PUT requests the entity to be replaced](https://stackoverflow.com/questions/21660791/what-is-the-main-difference-between-patch-and-put-request?answertab=oldest#tab-top). - ```txt PATCH my_api/v1/todos/1 ``` @@ -177,7 +174,7 @@ DELETE my_api/v1/todos/1 //Data source should only require the ID #### Return Appropriate Status Codes -Routes should also return proper status codes to the frontend based on the results of their operations. When an error occurs, sending a useful status and response to the client makes it clear what happened and allows the client to handle errors more smoothly. +Routes should also return proper status codes to the frontend based on the results of their operations. When an error occurs, sending a useful status and response to the client makes it clear what happened and allows the client to handle errors more smoothly. ```dart final todo = context.read().get(id); diff --git a/src/content/docs/architecture/index.md b/src/content/docs/architecture/index.md index 9e25729..cfae308 100644 --- a/src/content/docs/architecture/index.md +++ b/src/content/docs/architecture/index.md @@ -7,7 +7,7 @@ Layered architecture is used at VGV to build highly scalable, maintainable, and ## Layers -### Data layer +### Data Layer This is the lowest layer of the stack. It is the layer that is closest to the retrieval of data, hence the name. @@ -15,9 +15,9 @@ This is the lowest layer of the stack. It is the layer that is closest to the re The data layer is responsible for retrieving raw data from external sources and making it available to the [repository layer](#repository-layer). Examples of these external sources include an SQLite database, local storage, Shared Preferences, GPS, battery data, file system, or a RESTful API. -The data layer should be free of any specific domain or business logic. Ideally, packages within the data layer could be plugged into unreleated projects that need to retrieve data from the same sources. +The data layer should be free of any specific domain or business logic. Ideally, packages within the data layer could be plugged into unrelated projects that need to retrieve data from the same sources. -### Repository layer +### Repository Layer This compositional layer composes one or more data clients and applies "business rules" to the data. A separate repository is created for each domain, such as a user repository or a weather repository. Packages in this layer should not import any Flutter dependencies and not be dependent on other repositories. @@ -27,7 +27,7 @@ The repository layer is responsible for fetching data from one or more data sour > This layer can be considered the "product" layer. The business/product owner will determine the rules/acceptance criteria for how to combine data from one or more data providers into a unit that brings value to the customer. -### Business logic layer +### Business Logic Layer This layer composes one or more repositories and contains logic for how to surface the business rules via a specific feature or use-case. The business logic layer should have no dependency on the Flutter SDK and should not have direct dependencies on other business logic components. diff --git a/src/content/docs/code_style/code_style.md b/src/content/docs/code_style/code_style.md index e6285c3..af5e4fc 100644 --- a/src/content/docs/code_style/code_style.md +++ b/src/content/docs/code_style/code_style.md @@ -1,6 +1,6 @@ --- title: โœจ Code Style -description: Best practices for general code styling that goes beyond linter rules. +description: Best practices for general code styling that goes beyond linter rules. --- In general, the best guides for code style are the [Effective Dart](https://dart.dev/effective-dart) guidelines and the linter rules set up in [very_good_analysis](https://pub.dev/packages/very_good_analysis). However, there are certain practices we've learned outside of these two places that will make code more maintainable. @@ -14,7 +14,7 @@ Bad โ—๏ธ ```dart Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); -final userData = await getUserNameAndEmail(); +final userData = await getUserNameAndEmail(); // a bunch of other code... @@ -23,14 +23,14 @@ if (userData.$1.isValid) { } ``` -The above example will compile, but it is not immediately obvious what value `userData.$1` refers to here. The name of the function gives the reader the impression that the second value in the record is the email, but it is not clear. Particularly in a large codebase, where there could be more processing in between the call to `getUserNameAndEmail()` and the check on `userData.$1`, reviewers will not be able to tell immediately what is going on here. +The above example will compile, but it is not immediately obvious what value `userData.$1` refers to here. The name of the function gives the reader the impression that the second value in the record is the email, but it is not clear. Particularly in a large codebase, where there could be more processing in between the call to `getUserNameAndEmail()` and the check on `userData.$1`, reviewers will not be able to tell immediately what is going on here. Good โœ… ```dart Future<(String, String)> getUserNameAndEmail() async => return _someApiFetchMethod(); -final (username, email) = await getUserNameAndEmail(); +final (username, email) = await getUserNameAndEmail(); // a bunch of other code... diff --git a/src/content/docs/development/philosophy.mdx b/src/content/docs/development/philosophy.mdx index ef4dc11..a29ca66 100644 --- a/src/content/docs/development/philosophy.mdx +++ b/src/content/docs/development/philosophy.mdx @@ -167,4 +167,4 @@ As a team, we have many cumulative years of experience with apps of all kinds, f [declarative]: https://stackoverflow.com/a/15382180 [time-complexity]: https://en.wikipedia.org/wiki/Time_complexity [very-good-layered-architecture]: https://verygood.ventures/blog/very-good-flutter-architecture -[pub.dev]: https://pub.dev +[pub.dev]: https://pub.dev \ No newline at end of file diff --git a/src/content/docs/state_management/event_transformers.md b/src/content/docs/state_management/event_transformers.md index 4169fe4..ee85b51 100644 --- a/src/content/docs/state_management/event_transformers.md +++ b/src/content/docs/state_management/event_transformers.md @@ -1,7 +1,8 @@ --- -title: Bloc Event Transformers +title: ๐Ÿช„ Bloc Event Transformers description: Specifying the order in which Bloc events are handled. --- + Since [Bloc v.7.2.0](https://bloclibrary.dev/migration/#v720), events are handled concurrently by default. This allows event handler instances to execute simultaneously and provides no guarantees regarding the order of handler completion. Concurrent event handling is often desirable, but issues ranging from performance degradation to serious data and behavior defects can emerge if your specified event transformer diverges from the needs of your state management system. @@ -9,13 +10,14 @@ Concurrent event handling is often desirable, but issues ranging from performanc In particular, [race conditions](https://en.wikipedia.org/wiki/Race_condition) can produce bugs when the result of operations varies with their order of execution. #### Registering Event Transformers + Event transformers are specified in the `transformer` field of the event registration functions in the `Bloc` constructor: ```dart class MyBloc extends Bloc { MyBloc() : super(MyState()) { on( - _onEvent, + _onEvent, transformer: mySequentialTransformer(), ) on( @@ -25,31 +27,35 @@ class MyBloc extends Bloc { } } ``` -Each `on` statement creates a bucket for handling events of type `E`. + +Each `on` statement creates a bucket for handling events of type `E`. :::note -Note that event transformers are only applied within the bucket they are specified in. In the above example, only events of the same type (two of `MyEvent` or two `MySecondEvent`) would be processed sequentially, while a `MyEvent` and a `MySecondEvent` would be processed concurrently. +Note that event transformers are only applied within the bucket they are specified in. In the above example, only events of the same type (two of `MyEvent` or two `MySecondEvent`) would be processed sequentially, while a `MyEvent` and a `MySecondEvent` would be processed concurrently. ::: If you would like to enforce a global transformer scheme across event types, Joanna May's article ["How to Use Bloc With Streams and Concurrency"](https://verygood.ventures/blog/how-to-use-bloc-with-streams-and-concurrency) provides a concise guide. ### Transformer Types -The [Bloc Event Transformer API](https://bloclibrary.dev/bloc-concepts/#advanced-event-transformations) allows you to implement custom event transformers, but the [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) package furnishes several out-of-the box transformers which cover a wide range of use cases. These include: - - `concurrent` (default) - - `sequential` - - `droppable` - - `restartable` - +The [Bloc Event Transformer API](https://bloclibrary.dev/bloc-concepts/#advanced-event-transformations) allows you to implement custom event transformers, but the [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) package furnishes several out-of-the box transformers which cover a wide range of use cases. These include: + +- `concurrent` (default) +- `sequential` +- `droppable` +- `restartable` + Let's investigate the `sequential`, `droppable`, and `restartable` transformers and look at how they're used. #### Sequential + The `sequential` transformer ensures that events are handled one at a time, in a first in, first out order from when they are received. + ```dart class MyBloc extends Bloc { MyBloc() : super(MyState()) { on( - _onEvent, + _onEvent, transformer: sequential(), ) } @@ -76,21 +82,23 @@ class MoneyBloc extends Bloc { We then quickly add two events `ChangeBalance(add: 20)` and `ChangeBalance(add: 40)`, which will be handled concurrently. A possible sequence of events is: - - The first `ChangeBalance` handler instance will read a balance of `$100`, and send a not-yet-received request to the API to update the balance to `$120`. - - Before the first handler finishes its execution, the second handler executes, reads the old account value of `$100`, and completes an API request to update the balance to `$140`. - - Finally, the first handler's call to update the balance reaches the API, and the balance is now overwritten to `$120`. +- The first `ChangeBalance` handler instance will read a balance of `$100`, and send a not-yet-received request to the API to update the balance to `$120`. +- Before the first handler finishes its execution, the second handler executes, reads the old account value of `$100`, and completes an API request to update the balance to `$140`. +- Finally, the first handler's call to update the balance reaches the API, and the balance is now overwritten to `$120`. This example illustrates the issues that can arise from concurrent handling of operations. Had we used a `sequential` transformer for the `ChangeBalance` event handler and ensured that the first addition of $20 had completed before processing the next event, we wouldn't have lost $40. Note that when operations are safe to execute concurrently, using a `sequential` transformer can introduce unnecessary latency into event handling. #### Droppable -The `droppable` transformer will discard any events that are added while an event in that bucket is already being processed. + +The `droppable` transformer will discard any events that are added while an event in that bucket is already being processed. + ```dart class SayHiBloc extends Bloc { SayHiBloc() : super(SayHiState()) { on( - _onSayHello, + _onSayHello, transformer: droppable(), ) } @@ -99,21 +107,24 @@ class SayHiBloc extends Bloc { SayHello event, Emitter emit, ) async { - await api.say("Hello!"); + await api.say("Hello!"); } } ``` -In the above example, we'd like to avoid clogging up our API with unnecessary duplicate greetings. The `droppable` transformer will ensure that additional `SayHello` events added while the first `_onSayHello` instance is executing will be discarded and never executed. + +In the above example, we'd like to avoid clogging up our API with unnecessary duplicate greetings. The `droppable` transformer will ensure that additional `SayHello` events added while the first `_onSayHello` instance is executing will be discarded and never executed. Since events added during ongoing handling will be discarded by the `droppable` transformer, ensure that you're OK with any data stored in that event being lost. #### Restartable + The `restartable` transformer inverts the behavior of `droppable`, halting execution of previous event handlers in order to process the most recently received event. + ```dart class ThoughtBloc extends Bloc { ThoughtBloc() : super(ThoughtState()) { on( - _onThought, + _onThought, transformer: restartable(), ) } @@ -122,19 +133,22 @@ class ThoughtBloc extends Bloc { ThoughtEvent event, Emitter emit, ) async { - await api.record(event.thought); - emit( - state.copyWith( - message: 'This is my most recent thought: ${event.thought}', - ) - ); + await api.record(event.thought); + emit( + state.copyWith( + message: 'This is my most recent thought: ${event.thought}', + ) + ); } } ``` + If we want to avoid emitting the declaration that `${event.thought}` is my most recent thought when the bloc has received an even more recent thought, the `restartable` transformer will suspend `_onThought`'s processing of the outdated event if a more recent event is recieved during its execution. #### Testing Blocs + When writing tests for a bloc, you may encounter an issue where a variable event handling order is acceptable in use, but the inconsistent sequence of event execution makes the determined order of states required by `blocTest`'s `expect` field results in unpredictable test behavior: + ```dart blocTest( 'change value', @@ -149,9 +163,11 @@ blocTest( ], ); ``` + If the `ChangeValue(remove: 1)` event completes execution before `ChangeValue(add: 1)` has finished, the resultant states will instead be `MyState(value: -1),MyState(value: 0)`, causing the test to fail. Utilizing a `await Future.delayed(Duration.zero)` statement in the `act` function will ensure that the task queue is empty before additional events are added: + ```dart blocTest( 'change value', @@ -169,4 +185,5 @@ blocTest( ``` ### Conclusion + [`bloc_concurrency`](https://pub.dev/packages/bloc_concurrency) provides several event transformers to ensure that your bloc handles events in a manner that's conducive to the goals of your state management system. If `concurrent`, `sequential`, `droppable`, or `restartable` are insufficient for your purposes (for example if you would like a custom debouncing interval), you can always implement a custom [`EventTransformer`](https://bloclibrary.dev/bloc-concepts/#advanced-event-transformations) diff --git a/src/content/docs/testing/golden_file_testing.md b/src/content/docs/testing/golden_file_testing.md index c53a0df..5c4efbf 100644 --- a/src/content/docs/testing/golden_file_testing.md +++ b/src/content/docs/testing/golden_file_testing.md @@ -1,5 +1,5 @@ --- -title: Golden file testing +title: ๐Ÿ† Golden File Testing description: Golden testing best practices. --- diff --git a/src/content/docs/testing/testing.md b/src/content/docs/testing/testing.md index c08bcdc..eb2a78b 100644 --- a/src/content/docs/testing/testing.md +++ b/src/content/docs/testing/testing.md @@ -1,6 +1,8 @@ --- -title: Testing +title: ๐Ÿงช Testing Overview description: Testing best practices. +sidebar: + order: 1 --- At Very Good Ventures, our goal is to achieve 100% test coverage on all projects. Writing tests not only helps to reduce the number of bugs, but also encourages code to be written in a very clean, consistent, and maintainable way. While testing can initially add some additional time to the project, the trade-off is fewer bugs, higher confidence when shipping, and less time spent in QA cycles. diff --git a/src/content/docs/theming/theming.md b/src/content/docs/theming/theming.md index 21f7455..cd3801b 100644 --- a/src/content/docs/theming/theming.md +++ b/src/content/docs/theming/theming.md @@ -12,9 +12,10 @@ Flutter uses [Material Design](https://docs.flutter.dev/ui/design/material) with :::tip[Did you know?] Not everyone in the community is happy about Material and Cupertino being baked into the framework. Check out these discussions: -- https://github.com/flutter/flutter/issues/101479 -- https://github.com/flutter/flutter/issues/110195 - ::: +- +- + +::: ## Use ThemeData @@ -85,11 +86,11 @@ Let's break down typography into three sections: 1. [Importing Fonts](#importing-fonts) 2. [Custom Text Styles](#custom-text-styles) -3. [TextTheme](#text-theme) +3. [TextTheme](#texttheme) ### Importing Fonts -To keep things organzied, fonts are generally stored in an `assets` folder: +To keep things organized, fonts are generally stored in an `assets` folder: ```txt assets/ From f4fe43e06c24ccbf6152b2ec72b39a3c940eda62 Mon Sep 17 00:00:00 2001 From: Joanna May Date: Wed, 24 Jul 2024 10:07:33 -0500 Subject: [PATCH 2/2] fix: workflow --- .github/workflows/markdown_lint.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/markdown_lint.yml b/.github/workflows/markdown_lint.yml index ebe6bb5..316c9ad 100644 --- a/.github/workflows/markdown_lint.yml +++ b/.github/workflows/markdown_lint.yml @@ -14,4 +14,8 @@ jobs: with: node-version: 22 - - run: npm run lint + - name: Install dependencies + run: npm install + + - name: Run Markdown Lint + run: npm run lint