diff --git a/.all-contributorsrc b/.all-contributorsrc index 518deca..1ae6e5a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -3,7 +3,9 @@ "projectOwner": "tomalaforge", "repoType": "github", "repoHost": "https://github.com", - "files": ["README.md"], + "files": [ + "README.md" + ], "imageSize": 100, "commit": true, "commitConvention": "angular", @@ -13,31 +15,63 @@ "name": "Laforge Thomas", "avatar_url": "https://avatars.githubusercontent.com/u/30832608?v=4", "profile": "https://thomaslaforge.dev/home", - "contributions": ["code", "doc", "content", "ideas", "design"] + "contributions": [ + "code", + "doc", + "content", + "ideas", + "design" + ] }, { "login": "alan-bio", "name": "Alan Dragicevich", "avatar_url": "https://avatars.githubusercontent.com/u/31838230?v=4", "profile": "https://github.com/alan-bio", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "edimitchel", "name": "Michel EDIGHOFFER", "avatar_url": "https://avatars.githubusercontent.com/u/2922851?v=4", "profile": "https://github.com/edimitchel", - "contributions": ["doc"] + "contributions": [ + "doc" + ] }, { "login": "gsgonzalez88", "name": "Gerardo Sebastian Gonzalez", "avatar_url": "https://avatars.githubusercontent.com/u/39884678?v=4", "profile": "https://github.com/gsgonzalez88", - "contributions": ["doc"] + "contributions": [ + "doc" + ] + }, + { + "login": "marryday", + "name": "Evseev Yuriy", + "avatar_url": "https://avatars.githubusercontent.com/u/57489315?v=4", + "profile": "https://github.com/marryday", + "contributions": [ + "bug" + ] + }, + { + "login": "tomer953", + "name": "Tomer953", + "avatar_url": "https://avatars.githubusercontent.com/u/1807493?v=4", + "profile": "https://github.com/tomer953", + "contributions": [ + "bug", + "doc", + "code" + ] } ], "contributorsPerLine": 7, - "linkToUsage": true + "linkToUsage": true, + "commitType": "docs" } - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8fe2e7..44469ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,3 @@ # Contributing -> Thank you for considering contributing to this project. Your help is very much appreciated! - -When contributing, it's better to first explain the challenge/exercice you are thinking about in the issue tab. - -## Getting started - -Please follow those step in order to succesfully make your contribution to this repository. - -1. Fork the project -2. Install **Nx Console**, this will help you work with this repository -3. Run `npm ci` to install all dependencies -4. Generate a new app with Nx Console > Right Click on apps folder > `Nx Generate Application` -5. Copy/Paste **example.README.md** and fill it up. -6. Link the main **README** with your new challenge -7. Few days later, create a PR with your answer. -8. Optional: write a blog post explaining your Challenge and the solution you came up with. - -## Pull Request Process - -1. We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/) - in our commit messages, i.e. `feat(core): improve typing` -2. When you are ready, create Pull Request of your fork into original repository with the title starting with **NEW CHALLENGE** +Learn how to contribute [here](https://angular-challenges.vercel.app/guides/contribute/) diff --git a/README.md b/README.md index 578e3fe..84456c4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f ## Challenges -Check [all 35 challenges](https://angular-challenges.vercel.app/) +Check [all 36 challenges](https://angular-challenges.vercel.app/) ## Contributors ✨ @@ -38,6 +38,8 @@ Check [all 35 challenges](https://angular-challenges.vercel.app/) Alan Dragicevich
Alan Dragicevich

📖 Michel EDIGHOFFER
Michel EDIGHOFFER

📖 Gerardo Sebastian Gonzalez
Gerardo Sebastian Gonzalez

📖 + Evseev Yuriy
Evseev Yuriy

🐛 + Tomer953
Tomer953

🐛 📖 💻 diff --git a/README.test.md b/README.test.md deleted file mode 100644 index 01b86ba..0000000 --- a/README.test.md +++ /dev/null @@ -1,38 +0,0 @@ -

- -

- -

Collection of Angular TESTING challenges

- -
- -## Intro - -This project has been created with two purposes: - -- The first purpose is to assist you in becoming better at Testing using **Testing Library** and **Cypress Component Testing**. 💪 -- The second purpose is to share best practices and different implementations of the same problem to gain diverse perspectives. 📖 - -Thanks to all these challenges, you will learn how to create **Integration Tests** to test your UI. The goal is to test your component/application as a black box, just as an end user or developer would do. - -> **Learning by reading is good but learning by doing is better.** - -## Testing Challenges - -> Click the following badges to join your next challenge. -> -> Easy challenge -> Easy challenge -> Easy challenge - -
-testing - -router outlet TestingTesting a small application with a router -nested component Testing Testing a parent component with Child components -input output TestingTesting a presentational component by setting Inputs and listening to the Outputs -modal TestingTesting a modal -harness TestingTesting using Component Harnesses -Create harnessCreating a custom Component Harness and Testing your application by using this harness -Test a simple checkboxTesting a very simple checkbox -Test a real applicationTesting a real-life application by creating test double on Http request and handling all asynchronous part diff --git a/apps/performance/memoized/README.md b/apps/performance/memoized/README.md index c3ae1bc..4e250b9 100644 --- a/apps/performance/memoized/README.md +++ b/apps/performance/memoized/README.md @@ -1,32 +1,13 @@ -

memoized function

+# Memoization > Author: Thomas Laforge - +### Run Application -## Information +```bash +npx nx serve performance-memoized +``` -## Statement +### Documentation and Instruction -### Step 1 - -### Step 2 - -### Constraints: - -### Submitting your work - -1. Fork the project -2. clone it -3. npm ci -4. `npx nx serve memoized` -5. _...work on it_ -6. Commit your work -7. Submit a PR with a title beginning with **Answer:35** that I will review and other dev can review. - -memoized -memoized solution author - - - -_You can ask any question on_ twitter +Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular-performance/35-memoize/). diff --git a/apps/performance/ngfor-optimize/.eslintrc.json b/apps/performance/ngfor-optimize/.eslintrc.json new file mode 100644 index 0000000..bf8df14 --- /dev/null +++ b/apps/performance/ngfor-optimize/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/performance/ngfor-optimize/README.md b/apps/performance/ngfor-optimize/README.md new file mode 100644 index 0000000..6571704 --- /dev/null +++ b/apps/performance/ngfor-optimize/README.md @@ -0,0 +1,13 @@ +# NgFor Optimization + +> Author: Thomas Laforge + +### Run Application + +```bash +npx nx serve performance-ngfor-optimize +``` + +### Documentation and Instruction + +Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular-performance/36-ngfor-optimize/). diff --git a/apps/performance/ngfor-optimize/jest.config.ts b/apps/performance/ngfor-optimize/jest.config.ts new file mode 100644 index 0000000..5b2ccae --- /dev/null +++ b/apps/performance/ngfor-optimize/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'performance-ngfor-optimize', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/apps/performance/ngfor-optimize', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/apps/performance/ngfor-optimize/project.json b/apps/performance/ngfor-optimize/project.json new file mode 100644 index 0000000..c34d04f --- /dev/null +++ b/apps/performance/ngfor-optimize/project.json @@ -0,0 +1,99 @@ +{ + "name": "performance-ngfor-optimize", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/performance/ngfor-optimize/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/performance/ngfor-optimize", + "index": "apps/performance/ngfor-optimize/src/index.html", + "main": "apps/performance/ngfor-optimize/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/performance/ngfor-optimize/tsconfig.app.json", + "assets": [ + "apps/performance/ngfor-optimize/src/favicon.ico", + "apps/performance/ngfor-optimize/src/assets" + ], + "styles": [ + "apps/performance/ngfor-optimize/src/styles.scss", + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" + ], + "scripts": [], + "allowedCommonJsDependencies": ["seedrandom"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "performance-ngfor-optimize:build:production" + }, + "development": { + "browserTarget": "performance-ngfor-optimize:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "performance-ngfor-optimize:build" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "apps/performance/ngfor-optimize/**/*.ts", + "apps/performance/ngfor-optimize/**/*.html" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/performance/ngfor-optimize/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + } +} diff --git a/apps/performance/ngfor-optimize/src/app/app.component.ts b/apps/performance/ngfor-optimize/src/app/app.component.ts new file mode 100644 index 0000000..58d89f4 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/app.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { PersonService } from './list.service'; +import { PersonListComponent } from './person-list.component'; + +@Component({ + standalone: true, + imports: [ + PersonListComponent, + FormsModule, + MatFormFieldModule, + MatInputModule, + ], + providers: [PersonService], + selector: 'app-root', + template: ` +

+ List of Persons +

+ + + + + + + `, + host: { + class: 'flex items-center flex-col gap-5', + }, +}) +export class AppComponent implements OnInit { + readonly personService = inject(PersonService); + readonly persons = this.personService.persons; + + label = ''; + + ngOnInit(): void { + this.personService.loadPersons(); + } + + handleKey(event: any) { + if (event.keyCode === 13) { + this.personService.addPerson(this.label); + this.label = ''; + } + } +} diff --git a/apps/performance/ngfor-optimize/src/app/app.config.ts b/apps/performance/ngfor-optimize/src/app/app.config.ts new file mode 100644 index 0000000..59198e6 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/app.config.ts @@ -0,0 +1,6 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +export const appConfig: ApplicationConfig = { + providers: [provideAnimations()], +}; diff --git a/apps/performance/ngfor-optimize/src/app/generateList.ts b/apps/performance/ngfor-optimize/src/app/generateList.ts new file mode 100644 index 0000000..41b934e --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/generateList.ts @@ -0,0 +1,15 @@ +import { randEmail, randFirstName } from '@ngneat/falso'; +import { Person } from './person.model'; + +export function generateList() { + const arr: Person[] = []; + + for (let i = 0; i < 50; i++) { + arr.push({ + email: randEmail(), + name: randFirstName(), + }); + } + + return arr; +} diff --git a/apps/performance/ngfor-optimize/src/app/list.service.ts b/apps/performance/ngfor-optimize/src/app/list.service.ts new file mode 100644 index 0000000..e56bdde --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/list.service.ts @@ -0,0 +1,44 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { randEmail, randFirstName } from '@ngneat/falso'; +import { generateList } from './generateList'; +import { Person } from './person.model'; + +@Injectable() +export class PersonService { + private readonly fakeBackend = inject(FakeBackendService); + readonly persons = signal([]); + + loadPersons() { + this.persons.set(generateList()); + } + + deletePerson(email: string) { + this.persons.set( + this.fakeBackend + .returnNewList(this.persons()) + .filter((p) => p.email !== email) + ); + } + + updatePerson(email: string) { + this.persons.set( + this.fakeBackend + .returnNewList(this.persons()) + .map((p) => (p.email === email ? { email, name: randFirstName() } : p)) + ); + } + + addPerson(name: string) { + this.persons.set([ + { email: randEmail(), name }, + ...this.fakeBackend.returnNewList(this.persons()), + ]); + } +} + +@Injectable({ providedIn: 'root' }) +export class FakeBackendService { + returnNewList = (input: Person[]): Person[] => [ + ...input.map((i) => ({ ...i })), + ]; +} diff --git a/apps/performance/ngfor-optimize/src/app/person-list.component.ts b/apps/performance/ngfor-optimize/src/app/person-list.component.ts new file mode 100644 index 0000000..69aee34 --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/person-list.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { CommonModule } from '@angular/common'; +import { Person } from './person.model'; + +@Component({ + selector: 'app-person-list', + standalone: true, + imports: [CommonModule], + template: ` +
+

{{ person.name }}

+
+ + +
+
+ `, + host: { + class: 'w-full flex flex-col', + }, +}) +export class PersonListComponent { + @Input() persons: Person[] = []; + @Output() delete = new EventEmitter(); + @Output() update = new EventEmitter(); +} diff --git a/apps/performance/ngfor-optimize/src/app/person.model.ts b/apps/performance/ngfor-optimize/src/app/person.model.ts new file mode 100644 index 0000000..0ccffbb --- /dev/null +++ b/apps/performance/ngfor-optimize/src/app/person.model.ts @@ -0,0 +1,4 @@ +export interface Person { + email: string; + name: string; +} diff --git a/apps/performance/ngfor-optimize/src/assets/.gitkeep b/apps/performance/ngfor-optimize/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/performance/ngfor-optimize/src/favicon.ico b/apps/performance/ngfor-optimize/src/favicon.ico new file mode 100644 index 0000000..317ebcb Binary files /dev/null and b/apps/performance/ngfor-optimize/src/favicon.ico differ diff --git a/apps/performance/ngfor-optimize/src/index.html b/apps/performance/ngfor-optimize/src/index.html new file mode 100644 index 0000000..63fe2ad --- /dev/null +++ b/apps/performance/ngfor-optimize/src/index.html @@ -0,0 +1,13 @@ + + + + + performance-ngfor-optimize + + + + + + + + diff --git a/apps/performance/ngfor-optimize/src/main.ts b/apps/performance/ngfor-optimize/src/main.ts new file mode 100644 index 0000000..514c89a --- /dev/null +++ b/apps/performance/ngfor-optimize/src/main.ts @@ -0,0 +1,7 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); diff --git a/apps/performance/ngfor-optimize/src/styles.scss b/apps/performance/ngfor-optimize/src/styles.scss new file mode 100644 index 0000000..77e408a --- /dev/null +++ b/apps/performance/ngfor-optimize/src/styles.scss @@ -0,0 +1,5 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/performance/ngfor-optimize/src/test-setup.ts b/apps/performance/ngfor-optimize/src/test-setup.ts new file mode 100644 index 0000000..15de72a --- /dev/null +++ b/apps/performance/ngfor-optimize/src/test-setup.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'jest-preset-angular/setup-jest'; diff --git a/apps/performance/ngfor-optimize/tailwind.config.js b/apps/performance/ngfor-optimize/tailwind.config.js new file mode 100644 index 0000000..38183db --- /dev/null +++ b/apps/performance/ngfor-optimize/tailwind.config.js @@ -0,0 +1,14 @@ +const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind'); +const { join } = require('path'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'), + ...createGlobPatternsForDependencies(__dirname), + ], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/apps/performance/ngfor-optimize/tsconfig.app.json b/apps/performance/ngfor-optimize/tsconfig.app.json new file mode 100644 index 0000000..5822042 --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/performance/ngfor-optimize/tsconfig.editor.json b/apps/performance/ngfor-optimize/tsconfig.editor.json new file mode 100644 index 0000000..8ae117d --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["jest", "node"] + } +} diff --git a/apps/performance/ngfor-optimize/tsconfig.json b/apps/performance/ngfor-optimize/tsconfig.json new file mode 100644 index 0000000..e85865c --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/performance/ngfor-optimize/tsconfig.spec.json b/apps/performance/ngfor-optimize/tsconfig.spec.json new file mode 100644 index 0000000..1a4817a --- /dev/null +++ b/apps/performance/ngfor-optimize/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/apps/projection/src/app/data-access/fake-http.service.ts b/apps/projection/src/app/data-access/fake-http.service.ts index ac12467..e21ce03 100644 --- a/apps/projection/src/app/data-access/fake-http.service.ts +++ b/apps/projection/src/app/data-access/fake-http.service.ts @@ -36,7 +36,7 @@ export const randStudent = (): Student => ({ id: factoryStudent(), firstname: randFirstName(), lastname: randLastName(), - mainTeacher: teachers[randNumber({ max: teachers.length })], + mainTeacher: teachers[randNumber({ max: teachers.length - 1 })], school: randWord(), }); diff --git a/challenge-number.json b/challenge-number.json index 4fc2198..a6d987c 100644 --- a/challenge-number.json +++ b/challenge-number.json @@ -1,3 +1,6 @@ { - "total": 35 + "total": 36, + "🟢": 13, + "🟠": 116, + "🔴": 207 } diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 5be045c..7f60595 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -1,15 +1,8 @@ import starlight from '@astrojs/starlight'; -import vercel from '@astrojs/vercel/static'; import { defineConfig } from 'astro/config'; // https://astro.build/config export default defineConfig({ - output: 'static', - adapter: vercel({ - webAnalytics: { - enabled: true, - }, - }), integrations: [ starlight({ title: 'Angular Challenges', @@ -19,7 +12,7 @@ export default defineConfig({ }, favicon: './angular-challenge.ico', social: { - github: 'https://github.com/withastro/starlight', + github: 'https://github.com/tomalaforge/angular-challenges', linkedin: 'https://www.linkedin.com/in/thomas-laforge-2b05a945/', twitter: 'https://twitter.com/laforge_toma', }, diff --git a/docs/package-lock.json b/docs/package-lock.json index 6596539..9deac87 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.1", "dependencies": { "@astrojs/starlight": "^0.10.0", - "@astrojs/vercel": "^5.0.1", "@fontsource/ibm-plex-serif": "^5.0.8", "astro": "^3.0.6", "sharp": "^0.32.5" @@ -152,23 +151,6 @@ "node": ">=18.14.1" } }, - "node_modules/@astrojs/vercel": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-5.0.1.tgz", - "integrity": "sha512-qDXTBSRSzTRourx90QcB/uARUcABDRLfrDggNtyiyyh6WoJPK7BuhKDYh0Bd372Mrt2mm15ZSGaxHJLmbpv2wg==", - "dependencies": { - "@astrojs/internal-helpers": "0.2.0", - "@vercel/analytics": "^1.0.2", - "@vercel/nft": "^0.23.1", - "esbuild": "^0.19.2", - "fast-glob": "^3.3.1", - "set-cookie-parser": "^2.6.0", - "web-vitals": "^3.4.0" - }, - "peerDependencies": { - "astro": "^3.1.1" - } - }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -881,25 +863,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, "node_modules/@mdx-js/mdx": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-2.3.0.tgz", @@ -1025,23 +988,6 @@ "win32" ] }, - "node_modules/@rollup/pluginutils": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", - "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dependencies": { - "estree-walker": "^2.0.1", - "picomatch": "^2.2.2" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -1175,45 +1121,6 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", "integrity": "sha512-d0XxK3YTObnWVp6rZuev3c49+j4Lo8g4L1ZRm9z5L0xpoZycUPshHgczK5gsUMaZOstjVYYi09p5gYvUtfChYw==" }, - "node_modules/@vercel/analytics": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.0.2.tgz", - "integrity": "sha512-BZFxVrv24VbNNl5xMxqUojQIegEeXMI6rX3rg1uVLYUEXsuKNBSAEQf4BWEcjQDp/8aYJOj6m8V4PUA3x/cxgg==" - }, - "node_modules/@vercel/nft": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.23.1.tgz", - "integrity": "sha512-NE0xSmGWVhgHF1OIoir71XAd0W0C1UE3nzFyhpFiMr3rVhetww7NvM1kc41trBsPG37Bh+dE5FYCTMzM/gBu0w==", - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.5", - "@rollup/pluginutils": "^4.0.0", - "acorn": "^8.6.0", - "async-sema": "^3.1.1", - "bindings": "^1.4.0", - "estree-walker": "2.0.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.2", - "node-gyp-build": "^4.2.2", - "resolve-from": "^5.0.0" - }, - "bin": { - "nft": "out/cli.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@vercel/nft/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1233,17 +1140,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -1328,23 +1224,6 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -1497,11 +1376,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/async-sema": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", - "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==" - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -1516,11 +1390,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1571,14 +1440,6 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", @@ -1647,15 +1508,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -1943,14 +1795,6 @@ "simple-swizzle": "^0.2.2" } }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/color/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1981,16 +1825,6 @@ "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -2072,11 +1906,6 @@ "node": ">=4.0.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2367,11 +2196,6 @@ "reusify": "^1.0.4" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2412,38 +2236,6 @@ "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-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "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==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2462,62 +2254,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2547,25 +2283,6 @@ "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2643,11 +2360,6 @@ "node": ">=4" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "node_modules/hast-util-from-parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz", @@ -2856,18 +2568,6 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/human-signals": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", @@ -2915,15 +2615,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "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==", - "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", @@ -3347,28 +3038,6 @@ "node": ">=12" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/markdown-extensions": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", @@ -4513,17 +4182,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -4532,53 +4190,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4671,54 +4282,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp-build": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", - "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4757,17 +4325,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4779,14 +4336,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4964,14 +4513,6 @@ "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==", - "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", @@ -5497,14 +5038,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -5610,20 +5143,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "3.29.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", @@ -5748,16 +5267,6 @@ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==" }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" - }, "node_modules/sharp": { "version": "0.32.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz", @@ -6087,22 +5596,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -6123,19 +5616,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6155,11 +5635,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -6945,25 +6420,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/web-vitals": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.4.0.tgz", - "integrity": "sha512-n9fZ5/bG1oeDkyxLWyep0eahrNcPDF6bFqoyispt7xkW0xhDzpUBTgyDKqWDi1twT0MgH4HvvqzpUyh0ZxZV4A==" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6998,51 +6454,6 @@ "node": ">=4" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wide-align/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/widest-line": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", diff --git a/docs/package.json b/docs/package.json index ec3cdc1..069569d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "@astrojs/starlight": "^0.10.0", - "@astrojs/vercel": "^5.0.1", "@fontsource/ibm-plex-serif": "^5.0.8", "astro": "^3.0.6", "sharp": "^0.32.5" diff --git a/docs/src/assets/34/profiler-record.png b/docs/src/assets/angular-performance/34/profiler-record.png similarity index 100% rename from docs/src/assets/34/profiler-record.png rename to docs/src/assets/angular-performance/34/profiler-record.png diff --git a/docs/src/assets/angular-performance/35/memoize-profiler.png b/docs/src/assets/angular-performance/35/memoize-profiler.png new file mode 100644 index 0000000..0520fde Binary files /dev/null and b/docs/src/assets/angular-performance/35/memoize-profiler.png differ diff --git a/docs/src/assets/34/profiler-tab.png b/docs/src/assets/angular-performance/profiler-tab.png similarity index 100% rename from docs/src/assets/34/profiler-tab.png rename to docs/src/assets/angular-performance/profiler-tab.png diff --git a/docs/src/content/docs/challenges/angular-performance/12-scroll-cd.md b/docs/src/content/docs/challenges/angular-performance/12-scroll-cd.md index ed67c0b..5792c29 100644 --- a/docs/src/content/docs/challenges/angular-performance/12-scroll-cd.md +++ b/docs/src/content/docs/challenges/angular-performance/12-scroll-cd.md @@ -2,35 +2,39 @@ title: 🟠 Optimize Change Detection description: Challenge 12 about optimizing the number of change detection cycle while scrolling sidebar: - order: 12 + order: 107 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #12
## Information -In this challenge, you will need to optimize the change detection cycles run by Angular. +In Angular, there is a library called Zone.js that performs a lot of magic to simplify a developer's life. Zone.js monkey patches all DOM events so that it will recheck and rerender the view when something has changed inside the application. The developer doesn't have to manually trigger change detection. -Zone.js triggers a change detection cycle each time a scroll event is dispatched. However we only want to show or hide a button at a specific scroll position. Therefore, we only want to refresh our application once. +However, sometimes Zone.js triggers a lot more change detection than needed. For example, when you are listening to a scroll event, each scroll event will dispatch a new change detection cycle. -> You can vizualise how many times CD is triggered by installing the [Angular chrome devTool](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh) and starting a new recording on the profiler tab. +In this challenge, we only need to refresh the view at a specific scroll position to display or hide a button. All other cycles are unnecessary. -The following video will explain what is the goal of this challenge. +To have a better visualization of the problem, profile your application with Angular Dev Tools. + +:::note +If you don't know how to use it, read [the performance introduction page](/challenges/angular-performance/) first and come back after. +::: + +You can learn more details about zone pollution and how to resolve it [here](https://angular.io/guide/change-detection-zone-pollution). + +The following video will explain more in-depth the issue of this application. ## Statement -Your goal for this challenge is to avoid all unnecessary change detection cycles and trigger a CD only when needed. +Your goal for this challenge is to avoid all unnecessary change detection cycles and trigger a change detection only when needed. ## Constraint: -You cannot opt-out of zone.js. If this code is part of a large project and you opt out of zone.js, you will break many things within your application. +You cannot opt-out of Zone.js globally. If this code is part of a large project and you opt out of Zone.js, you will break your application without any doubt. --- @@ -49,7 +53,7 @@ Your PR title must start with Answer:12. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular-performance/34-default-onpush.md b/docs/src/content/docs/challenges/angular-performance/34-default-onpush.md index 6ea3f97..2aa2ff1 100644 --- a/docs/src/content/docs/challenges/angular-performance/34-default-onpush.md +++ b/docs/src/content/docs/challenges/angular-performance/34-default-onpush.md @@ -2,36 +2,40 @@ title: 🟢 Default vs OnPush description: Challenge 34 is about learning the difference between Default and OnPush Change Detection Strategy. sidebar: - order: 34 + order: 7 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #34
## Information -In this series of challenges, you will learn how to optimize and enhance the performance of your Angular Application. +In this challenge, we will explore the differences and impacts of using `ChangeDetectionStrategy.Default` versus `ChangeDetectionStrategy.OnPush`. -The first step is to download the [Angular DevTools Chrome extention](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh) if you haven't already done so. This extension allows you to profile your application and detect performance issues. +You can read the [Angular documentation](https://angular.io/guide/change-detection-skipping-subtrees) to learn more about the differences between these strategies. -In this challenge, we will explore the differences and impacts of using `ChangeDetectionStrategy.Default` versus `ChangeDetectionStrategy.OnPush`. To provide a clearer demonstration, I have added color enlightment to each component and each row in our application. However, in real-world scenarios, you will not have such visualization. This is where the Angular DevTool profiler comes to the rescue. +In this challenge, all components start with the `Default` strategy. When you type letters inside the input field, you will notice that all components are highlighted in orange. -Start by serving this application by running: `npx nx serve performance-default-onpush` inside your terminal. Then open Chrome DevTool by pressing **F12** and switch to the Angular Tab. From there you can select the Profiler tab as shown below. +:::note +I added color highlighting to each component and each row to provide a better visualization of when a component is rerendered. +::: -![profiler tab](../../../../assets/34/profiler-tab.png 'Profiler tab') +As you can see, each letter triggers a new change detection cycle, and all components are rerendered, causing performance issues. -Start profiling your application and type some letters inside the input field. You will notice that each element of your application will flash at each change detection cycle and the profiler will show you a bar for each change detection cycle. +Let's use the Angular DevTool to profile our application and understand how this tool can help us understand what is happening inside our application. -If you click on one of the bars (indicated by the yellow arrow on the picture below), you can see that `PersonListComponent`, `RandomComponent` and all the `MatListItem` are impacted by the change detection cycle, even when we only interact with the input field. +:::note +If you don't know how to use it, read [the performance introduction page](/challenges/angular-performance/) first and come back after. +::: -![profiler record](../../../../assets/34/profiler-record.png 'Profiler Record') +Now, start profiling your application and type some letters inside the input field to trigger some change detection cycles. + +If you click on one of the bars (indicated by the yellow arrow in the picture below), you can see that `PersonListComponent`, `RandomComponent`, and all the `MatListItem` are impacted by the change detection cycle, even when we only interact with the input field. + +![profiler record](../../../../assets/angular-performance/34/profiler-record.png 'Profiler Record') ## Statement -The goal of this challenge is to improve the clustering of change detection within the application. +The goal of this challenge is to improve the clustering of change detection within the application using the `OnPush` change detection strategy, but not only... ## Hints: @@ -66,7 +70,7 @@ Your PR title must start with Answer:34. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular-performance/35-memoize.md b/docs/src/content/docs/challenges/angular-performance/35-memoize.md new file mode 100644 index 0000000..54fa6aa --- /dev/null +++ b/docs/src/content/docs/challenges/angular-performance/35-memoize.md @@ -0,0 +1,70 @@ +--- +title: 🟢 Memoization +description: Challenge 35 is about learning +sidebar: + order: 8 +--- + +
Challenge #35
+ +## Information + +In Angular, pure Pipes are very powerful because the value is memoized, which means if the input value doesn't change, the `transform` function of the pipe is not recomputed, and the cached value is outputted. + +You can learn more about pipes in the [Angular documentation](https://angular.io/guide/pipes) and inside this [deep dive article](https://medium.com/ngconf/deep-dive-into-angular-pipes-c040588cd15d). + +In this challenge, we start with a button to load a list of people. Each person is associated with a number, and we will use the Fibonacci calculation to create a heavy computation that will slow down the application. + +Once the list is loaded, try typing some letters inside the input field. You will notice that the application is very slow, even though you are only performing very basic typing. + +:::note +We will not focus on the initial loading of the list in this challenge. +::: + +Let's use the Angular DevTool to profile our application and understand how this tool can help us understand what is happening inside our application. + +:::note +If you don't know how to use it, read [the performance introduction page](/challenges/angular-performance/) first and come back after. +::: + +Now, start profiling your application and type some letters inside the input field. You will see some red bars showing up inside the profiler panel. + +If you click on one of the bars (indicated by the yellow arrow in the picture below), you will see that the change detection cycle is taking more than 3s in `PersonListComponent`. + +![profiler record](../../../../assets/angular-performance/35/memoize-profiler.png 'Profiler Record') + +## Statement + +The goal of this challenge is to understand what is causing this latency and to improve it. + +## Hints: + +
+ Hint 1 + +Use `Pipes` to memoize the Fibonnaci computation. + +
+ +--- + +:::note +Start the project by running: `npx nx serve performance-memoized`. +::: + +:::tip[Reminder] +Your PR title must start with Answer:35. +::: + + diff --git a/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md b/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md new file mode 100644 index 0000000..2732592 --- /dev/null +++ b/docs/src/content/docs/challenges/angular-performance/36-ngfor-optimize.md @@ -0,0 +1,54 @@ +--- +title: 🟢 NgFor Optimization +description: Challenge 36 is about ... +sidebar: + order: 13 + badge: New +--- + +
Challenge #36
+ +## Information + +In this application, we have a list of individuals that we can add, delete or update. If you open the developer Chrome panel by pressing **F12**, go to he source tab, and expand the element to see the list, you will notice that each time, you add, delete or update a list item, the entire DOM elements are destroyed and initialized again. (See video below). + + + +We can also use the Angular DevTool to profile our application and understand what is happening inside our application. I will show you how to do it inside the following video. + + + +:::note +If you don't know how to use it, read [the performance introduction page](/challenges/angular-performance/) first and come back after. +::: + +If you need more information about `NgFor`, I invite you to read the [documentation](https://angular.io/api/common/NgFor) first. + +## Statement + +The goal of this challenge is to understand what is causing this DOM refresh and to solve it. + +--- + +:::note +Start the project by running: `npx nx serve ngfor-optimize`. +::: + +:::tip[Reminder] +Your PR title must start with Answer:36. +::: + + diff --git a/docs/src/content/docs/challenges/angular-performance/index.mdx b/docs/src/content/docs/challenges/angular-performance/index.mdx new file mode 100644 index 0000000..5d29f4a --- /dev/null +++ b/docs/src/content/docs/challenges/angular-performance/index.mdx @@ -0,0 +1,48 @@ +--- +title: Angular Performance +prev: false +next: false +description: Learn how to use the Angular Devtool chrome extension. +sidebar: + order: 1 +--- + +import { LinkCard } from '@astrojs/starlight/components'; + +In this series of challenges about performance, you will learn how to optimize and enhance the performance of your Angular application. + +Before starting to resolve any challenge, I invite you to download the [Angular DevTools Chrome extention](https://chrome.google.com/webstore/detail/angular-devtools/ienfalfjdbdpebioblfackkekamfmbnh) if you haven't already done so. + +This extension allows you to profile your application and detect performance issues, which is very useful for understanding where performance issues can occur. + +## How to use it + +When you serve an Angular application, you can inspect a page by pressing F12, which will open the Chrome developer tools. Then navigate to the Angular tab. From there, you can select the Profiler tab as shown below. + +![profiler tab](../../../../assets/angular-performance/profiler-tab.png 'Profiler tab') + +You can now profile your application by clicking on the record button. You can play with your application and see when change detection is triggered and which components are rerendered. + +:::tip[Learn more] +You can learn more on the [documentation page](https://angular.io/guide/devtools). +::: + +Now that you know how to use the Angular DevTool, you can choose a challenge, profile it, and resolve it. + + + + + + diff --git a/docs/src/content/docs/challenges/angular/1-projection.md b/docs/src/content/docs/challenges/angular/1-projection.md index d8998bd..8a280b5 100644 --- a/docs/src/content/docs/challenges/angular/1-projection.md +++ b/docs/src/content/docs/challenges/angular/1-projection.md @@ -56,7 +56,16 @@ Your PR title must start with Answer:1. target="_blank" rel="noopener noreferrer" alt="Projection blog article"> - + Blog Post + + + Video + 🇫🇷 + diff --git a/docs/src/content/docs/challenges/angular/10-pipe-utility.md b/docs/src/content/docs/challenges/angular/10-pipe-utility.md index 567915e..d9cc5dc 100644 --- a/docs/src/content/docs/challenges/angular/10-pipe-utility.md +++ b/docs/src/content/docs/challenges/angular/10-pipe-utility.md @@ -2,7 +2,7 @@ title: 🔴 Utility Wrapper Pipe description: Challenge 10 is about creating a pipe to wrap utilities sidebar: - order: 10 + order: 202 --- :::note @@ -40,7 +40,7 @@ Your PR title must start with Answer:10. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/13-styling.md b/docs/src/content/docs/challenges/angular/13-styling.md index d751036..431452c 100644 --- a/docs/src/content/docs/challenges/angular/13-styling.md +++ b/docs/src/content/docs/challenges/angular/13-styling.md @@ -2,7 +2,7 @@ title: 🟠 Highly Customizable CSS description: Challenge 13 is about creating highly customizable CSS styles sidebar: - order: 13 + order: 104 --- :::note @@ -42,7 +42,7 @@ Your PR title must start with Answer:13. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/16-di.md b/docs/src/content/docs/challenges/angular/16-di.md index e90410d..1897bfe 100644 --- a/docs/src/content/docs/challenges/angular/16-di.md +++ b/docs/src/content/docs/challenges/angular/16-di.md @@ -2,7 +2,7 @@ title: 🔴 Master Dependancy Injection description: Challenge 16 is about masjering how dependancy injection works sidebar: - order: 16 + order: 203 --- :::note @@ -45,7 +45,7 @@ Your PR title must start with Answer:16. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/21-achor-scrolling.md b/docs/src/content/docs/challenges/angular/21-achor-scrolling.md index 7c1ace1..1a3c1ce 100644 --- a/docs/src/content/docs/challenges/angular/21-achor-scrolling.md +++ b/docs/src/content/docs/challenges/angular/21-achor-scrolling.md @@ -2,7 +2,7 @@ title: 🟢 Anchor Navigation description: Challenge 21 is about navigating inside the page with anchor sidebar: - order: 21 + order: 4 --- :::note @@ -37,7 +37,7 @@ Your PR title must start with Answer:21. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/22-router-input.md b/docs/src/content/docs/challenges/angular/22-router-input.md index 9b4b72c..b0fa706 100644 --- a/docs/src/content/docs/challenges/angular/22-router-input.md +++ b/docs/src/content/docs/challenges/angular/22-router-input.md @@ -2,7 +2,7 @@ title: 🟢 @RouterInput() description: Challenge 22 is about using the @Input decorator to retreive router params. sidebar: - order: 22 + order: 5 --- :::note @@ -32,7 +32,7 @@ Your PR title must start with Answer:22. ❖ Community Answers ▶︎ Author Answer @@ -41,7 +41,7 @@ Your PR title must start with Answer:22. target="_blank" rel="noopener noreferrer" alt="@RouterInput() blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/angular/3-directive-enhancement.md b/docs/src/content/docs/challenges/angular/3-directive-enhancement.md index 7581ef5..a76b566 100644 --- a/docs/src/content/docs/challenges/angular/3-directive-enhancement.md +++ b/docs/src/content/docs/challenges/angular/3-directive-enhancement.md @@ -2,7 +2,7 @@ title: 🟠 Directive Enhancement description: Challenge 3 is about enhancing a built-in directive sidebar: - order: 3 + order: 101 --- :::note @@ -60,7 +60,7 @@ Your PR title must start with Answer:3. ❖ Community Answers ▶︎ Author Answer @@ -69,7 +69,7 @@ Your PR title must start with Answer:3. target="_blank" rel="noopener noreferrer" alt="Directive Enhancement blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/angular/30-interop-rxjs-signal.md b/docs/src/content/docs/challenges/angular/30-interop-rxjs-signal.md index 3229bc9..ddf26ac 100644 --- a/docs/src/content/docs/challenges/angular/30-interop-rxjs-signal.md +++ b/docs/src/content/docs/challenges/angular/30-interop-rxjs-signal.md @@ -2,7 +2,7 @@ title: 🔴 Interoperability Rxjs/Signal description: Challenge 30 is about learning how to mix signal with Rxjs sidebar: - order: 30 + order: 204 --- :::note @@ -34,7 +34,7 @@ Your PR title must start with Answer:30. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/31-module-to-standalone.md b/docs/src/content/docs/challenges/angular/31-module-to-standalone.md index e356ec7..b98131f 100644 --- a/docs/src/content/docs/challenges/angular/31-module-to-standalone.md +++ b/docs/src/content/docs/challenges/angular/31-module-to-standalone.md @@ -2,7 +2,7 @@ title: 🟢 Module to Standalone description: Challenge 31 is about migrating a module based application to a standalone application. sidebar: - order: 31 + order: 6 --- :::note @@ -42,7 +42,7 @@ Your PR title must start with Answer:31. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular-performance/32-bug-cd.md b/docs/src/content/docs/challenges/angular/32-bug-cd.md similarity index 93% rename from docs/src/content/docs/challenges/angular-performance/32-bug-cd.md rename to docs/src/content/docs/challenges/angular/32-bug-cd.md index dadf4d0..6a4f458 100644 --- a/docs/src/content/docs/challenges/angular-performance/32-bug-cd.md +++ b/docs/src/content/docs/challenges/angular/32-bug-cd.md @@ -2,7 +2,7 @@ title: 🟠 Change Detection Bug description: Challenge 32 is about debugging an application that has issue when change detection is triggered sidebar: - order: 32 + order: 105 --- :::note @@ -11,11 +11,13 @@ WIP: The following documentation will be reviewed and improved. However, you can
Challenge #32
+:::note This challenge is inspired by a real-life example that I simplified to create this nice challenge. +::: ## Information -In this small application, we have a navigation menu to route our application to either `barComponent` or `FooComponent`. However our application is not loading and no errors are displayed inside the console. +In this small application, we have a navigation menu to route our application to either `BarComponent` or `FooComponent`. However our application is not loading and no errors are displayed inside the console. ## Statement @@ -53,7 +55,7 @@ Your PR title must start with Answer:32. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/33-decoupling.md b/docs/src/content/docs/challenges/angular/33-decoupling.md index e7eba9f..c317820 100644 --- a/docs/src/content/docs/challenges/angular/33-decoupling.md +++ b/docs/src/content/docs/challenges/angular/33-decoupling.md @@ -2,7 +2,7 @@ title: 🟠 Decoupling Components description: Challenge 33 is about decoupling two strongly coupled components using Injection Token sidebar: - order: 33 + order: 106 --- :::note @@ -50,7 +50,7 @@ Your PR title must start with Answer:33. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/angular/4-context-outlet-typed.md b/docs/src/content/docs/challenges/angular/4-context-outlet-typed.md index 9eb5c60..1ab2ab0 100644 --- a/docs/src/content/docs/challenges/angular/4-context-outlet-typed.md +++ b/docs/src/content/docs/challenges/angular/4-context-outlet-typed.md @@ -2,7 +2,7 @@ title: 🔴 Typed ContextOutlet description: Challenge 4 is about strongly typing ngContextOutlet directives sidebar: - order: 4 + order: 201 --- :::note @@ -58,7 +58,7 @@ Your PR title must start with Answer:4. ❖ Community Answers ▶︎ Author Answer @@ -67,7 +67,7 @@ Your PR title must start with Answer:4. target="_blank" rel="noopener noreferrer" alt="Typed ContextOutlet blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/angular/5-crud.md b/docs/src/content/docs/challenges/angular/5-crud.md index a41f8b3..2bde197 100644 --- a/docs/src/content/docs/challenges/angular/5-crud.md +++ b/docs/src/content/docs/challenges/angular/5-crud.md @@ -2,7 +2,7 @@ title: 🟢 Crud application description: Challenge 5 is about refactoring a crud application sidebar: - order: 5 + order: 2 --- :::note @@ -67,12 +67,12 @@ Your PR title must start with Answer:5. diff --git a/docs/src/content/docs/challenges/angular/8-pipe-pure.md b/docs/src/content/docs/challenges/angular/8-pipe-pure.md index 540f385..e09db9e 100644 --- a/docs/src/content/docs/challenges/angular/8-pipe-pure.md +++ b/docs/src/content/docs/challenges/angular/8-pipe-pure.md @@ -2,7 +2,7 @@ title: 🟢 Pure Pipe description: Challenge 8 is about creating a pure pipe sidebar: - order: 8 + order: 3 --- :::note @@ -40,7 +40,7 @@ Your PR title must start with Answer:8. ❖ Community Answers ▶︎ Author Answer @@ -49,7 +49,7 @@ Your PR title must start with Answer:8. target="_blank" rel="noopener noreferrer" alt="Pure Pipe blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/angular/9-pipe-wrapFn.md b/docs/src/content/docs/challenges/angular/9-pipe-wrapFn.md index 0893b53..427a4d6 100644 --- a/docs/src/content/docs/challenges/angular/9-pipe-wrapFn.md +++ b/docs/src/content/docs/challenges/angular/9-pipe-wrapFn.md @@ -2,7 +2,7 @@ title: 🟠 Wrap Function Pipe description: Challenge 9 is about creating a pipe to wrap component fonctions sidebar: - order: 9 + order: 103 --- :::note @@ -41,7 +41,7 @@ Your PR title must start with Answer:9. ❖ Community Answers ▶︎ Author Answer @@ -50,7 +50,7 @@ Your PR title must start with Answer:9. target="_blank" rel="noopener noreferrer" alt="Wrap Function Pipe blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/ngrx/2-effect-selector.md b/docs/src/content/docs/challenges/ngrx/2-effect-selector.md index bf0210b..1c08893 100644 --- a/docs/src/content/docs/challenges/ngrx/2-effect-selector.md +++ b/docs/src/content/docs/challenges/ngrx/2-effect-selector.md @@ -2,7 +2,7 @@ title: 🟠 Effect vs Selector description: Challenge 2 is about learning the difference between effects and selectors in NgRx sidebar: - order: 2 + order: 113 --- :::note @@ -48,7 +48,7 @@ Your PR title must start with Answer:2. ❖ Community Answers ▶︎ Author Answer @@ -57,7 +57,7 @@ Your PR title must start with Answer:2. target="_blank" rel="noopener noreferrer" alt="Effect vs Selector blog article"> - + Blog Post diff --git a/docs/src/content/docs/challenges/ngrx/7-power-effect.md b/docs/src/content/docs/challenges/ngrx/7-power-effect.md index 2081d02..8eee6a9 100644 --- a/docs/src/content/docs/challenges/ngrx/7-power-effect.md +++ b/docs/src/content/docs/challenges/ngrx/7-power-effect.md @@ -2,7 +2,7 @@ title: 🔴 Power of Effect description: Challenge 7 is about creating an Ngrx effect with another Rxjs Hot observable sidebar: - order: 7 + order: 206 --- :::note @@ -51,7 +51,7 @@ Your PR title must start with Answer:7. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/nx/25-generator-lib-ext.md b/docs/src/content/docs/challenges/nx/25-generator-lib-ext.md index d1416ae..ca072bb 100644 --- a/docs/src/content/docs/challenges/nx/25-generator-lib-ext.md +++ b/docs/src/content/docs/challenges/nx/25-generator-lib-ext.md @@ -2,7 +2,7 @@ title: 🔴 Extend Lib Generator description: Challenge 25 is about creating a Nx generator to extend the built-in Library Generator sidebar: - order: 25 + order: 207 --- :::note @@ -67,7 +67,7 @@ Your PR title must start with Answer:25. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/nx/26-generator-comp.md b/docs/src/content/docs/challenges/nx/26-generator-comp.md index 42bf032..25045d1 100644 --- a/docs/src/content/docs/challenges/nx/26-generator-comp.md +++ b/docs/src/content/docs/challenges/nx/26-generator-comp.md @@ -2,7 +2,7 @@ title: 🟠 Component Generator description: Challenge 26 is about creating a Nx generator to create a custom component sidebar: - order: 26 + order: 116 --- :::note @@ -155,7 +155,7 @@ Your PR title must start with Answer:26. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/nx/27-forbid-enum-rule.md b/docs/src/content/docs/challenges/nx/27-forbid-enum-rule.md index 8b4df42..fbc25ff 100644 --- a/docs/src/content/docs/challenges/nx/27-forbid-enum-rule.md +++ b/docs/src/content/docs/challenges/nx/27-forbid-enum-rule.md @@ -2,7 +2,7 @@ title: 🟢 Custom Eslint Rule description: Challenge 27 is about creating a custom Eslint Rule to forbid enums sidebar: - order: 27 + order: 12 --- :::note @@ -36,7 +36,7 @@ Your PR title must start with Answer:27. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/rxjs/11-bug-chaining-operator.md b/docs/src/content/docs/challenges/rxjs/11-bug-chaining-operator.md index 5529c31..ef4beee 100644 --- a/docs/src/content/docs/challenges/rxjs/11-bug-chaining-operator.md +++ b/docs/src/content/docs/challenges/rxjs/11-bug-chaining-operator.md @@ -2,7 +2,7 @@ title: 🟠 High Order Operator Bug description: Challenge 11 is about resolving a Rxjs bug because of high order operators sidebar: - order: 11 + order: 114 --- :::note @@ -48,7 +48,7 @@ Your PR title must start with Answer:11. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/rxjs/14-race-condition.md b/docs/src/content/docs/challenges/rxjs/14-race-condition.md index 40b2243..36646d1 100644 --- a/docs/src/content/docs/challenges/rxjs/14-race-condition.md +++ b/docs/src/content/docs/challenges/rxjs/14-race-condition.md @@ -2,7 +2,7 @@ title: 🟢 Race Condition description: Challenge 14 is about race condition in Rxjs sidebar: - order: 14 + order: 11 --- :::note @@ -46,7 +46,7 @@ Your PR title must start with Answer:14. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/17-router.md b/docs/src/content/docs/challenges/testing/17-router.md index 4694d0f..982d3d4 100644 --- a/docs/src/content/docs/challenges/testing/17-router.md +++ b/docs/src/content/docs/challenges/testing/17-router.md @@ -2,34 +2,26 @@ title: 🟠 Router description: Challenge 17 is about testing the router sidebar: - order: 17 + order: 108 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #17
## Information -Testing is a crucial step in building scalable, maintainable, and trustworthy applications. -Testing should never be avoided, even in the face of short deadlines or strong pressure from the product team. -Nowadays, there are numerous awesome tools available that make it easy to test your code and provide a great developer experience. +We have a functional application that lists available books for borrowing inside a library. If the book you searched for is available, you will be directed to the corresponding book(s), otherwise, you will end up on an error page. -In this series of testing exercises, we will learn and master [Testing Library](https://testing-library.com/docs/) and [Cypress Component Testing](https://docs.cypress.io/guides/component-testing/angular/overview) that simplifies DOM manipulation for testing any Angular component. - -## Statement: - -We have a functional application that lists available books for borrowing inside a library. If the book you searched is available, you will be directed to the corresponding book(s), otherwise, you will end up on an error page. - -The goal is to test this behavior with Testing library and Cypress - -The file named `app.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-router-outlet`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. +The file named `app.component.spec.ts` will let you test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-router-outlet`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. For testing cypress, you will execute your test inside the `app.component.cy.ts` and run `npx nx component-test testing-router-outlet` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. -I created some `it` blocks but feel free to add more test if you like to. +# Statement + +The goal is to test multiple behaviors of the application described in each test file using Testing library and Cypress Component Testing. + +:::note +I have created some `it` blocks but feel free to add more tests if you want. +::: --- @@ -48,7 +40,7 @@ Your PR title must start with Answer:17. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/18-nested-comp.md b/docs/src/content/docs/challenges/testing/18-nested-comp.md index 6d41598..0a81533 100644 --- a/docs/src/content/docs/challenges/testing/18-nested-comp.md +++ b/docs/src/content/docs/challenges/testing/18-nested-comp.md @@ -2,28 +2,30 @@ title: 🟠 Nested Components description: Challenge 18 is about testing nested components sidebar: - order: 18 + order: 109 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #18
-## Statement: +## Information + +We have a small application that sends a title, typed into an input to a fake backend. +If the title is correctly typed, you can send the request otherwise you receive an error and the request is not sent. +The application is created with nested components. `ChildComponent` is the container that includes four components: `ResultComponent`, `ButtonComponent`, `InputComponent` and `ErrorComponent`. However since we are testing our component as a black box, the architecture of our components doesn't change anything. You can create your test, change how the components are structured, and your tests should still pass. That's the goal of integration tests. Never test internal implementation details!!!. -We have a small application that send a title to a fake backend that you type inside a input. -If the title is correctly typed, you can send the request otherwise you get a nice error and the request is not sent. You can play with it by running : `npx nx serve testing-nested`. -The goal is to test this behavior with Testing library and Cypress - The file named `child.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-nested`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. For testing cypress, you will execute your test inside the `child.component.cy.ts` and run `npx nx component-test testing-nested` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. -I created some `it` blocks but feel free to add more test if you like to. +# Statement + +The goal is to test multiple behaviors of the application describe inside each test files using Testing library and Cypress Component Testing. + +:::note +I have created some `it` blocks but feel free to add more tests if you want. +::: --- @@ -42,7 +44,7 @@ Your PR title must start with Answer:18. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/19-input-output.md b/docs/src/content/docs/challenges/testing/19-input-output.md index ac64921..baf6901 100644 --- a/docs/src/content/docs/challenges/testing/19-input-output.md +++ b/docs/src/content/docs/challenges/testing/19-input-output.md @@ -2,27 +2,28 @@ title: 🟠 Input Output description: Challenge 19 is about testing inputs and ouputs sidebar: - order: 19 + order: 110 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #19
-## Statement: +## Information: + +We have a small counter application that increments or decrements a number. The `CounterComponent` takes an initial value as an `@Input` and emits the result of the counter as an `@Output` when we click on the **Send** button. Since we are testing our component as a black box, we only have access to our inputs and listen to the output values. We should not rely on any internal implementation details!!! -We have a small counter application that increment or decrement a number. You can play with it by running : `npx nx serve testing-input-output`. -The goal is to test `CounterComponent` with Testing library and Cypress +The file named `counter.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-input-output`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. -The file named `counter.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-nested`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. +For testing cypress, you will execute your test inside the `child.component.cy.ts` and run `npx nx component-test testing-input-output` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. -For testing cypress, you will execute your test inside the `counter.component.cy.ts` and run `npx nx component-test testing-nested` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. +# Statement -I created some `it` blocks but feel free to add more test if you like to. +The goal is to test multiple behaviors of the application describe inside each test files using Testing library and Cypress Component Testing. + +:::note +I have created some `it` blocks but feel free to add more tests if you want. +::: --- @@ -41,7 +42,7 @@ Your PR title must start with Answer:19. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/20-modal.md b/docs/src/content/docs/challenges/testing/20-modal.md index 20a67ef..813e7b1 100644 --- a/docs/src/content/docs/challenges/testing/20-modal.md +++ b/docs/src/content/docs/challenges/testing/20-modal.md @@ -2,30 +2,32 @@ title: 🟠 Modal description: Challenge 20 is about testing modals sidebar: - order: 20 + order: 111 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #20
-## Statement: +## Information: -The goal of this challenge is to test dialogs inside your application. -Within this program, you will get an error modal if the user doesn't input a name, while a confirmation modal will appear in all other cases. -In the confirmation modal, if you click the "confirm" button, a message confirming the submission of the form will appear. Otherwise, if the user clicks on "Cancel", an error message will be displayed. +In this small application, you have an input prompting you to enter a name, and a **Confirm** button to submit your form. +If you enter a name, a confirmation modal will appear; otherwise an error modal will be displayed. +In the confirmation modal, if you click the **Confirm** button, a message confirming the submission of the form will appear. If the user clicks on **Cancel**, an error message will be displayed. + +The goal of this challenge is to test the dialogs inside your application. To do so, we will test the full application like an end-to-end test will do. This means, we will test the `AppComponent` as a black box and react to events on the page. No internal details should be tested. The difference between an e2e test and integration test is that we will mock all API calls. _(All http requests are faked inside this application, but this would not be the case in a real entreprice application.)_ You can play with it by running : `npx nx serve testing-modal`. -The goal is to test this behavior with Testing library and Cypress - The file named `app.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-modal`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. For testing cypress, you will execute your test inside the `app.component.cy.ts` and run `npx nx component-test testing-modal` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. -I created some `it` blocks but feel free to add more test if you like to. +# Statement + +The goal is to test multiple behaviors of the application describe inside each test files using Testing library and Cypress Component Testing. + +:::note +I have created some `it` blocks but feel free to add more tests if you want. +::: --- @@ -44,7 +46,7 @@ Your PR title must start with Answer:20. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/23-harness.md b/docs/src/content/docs/challenges/testing/23-harness.md index 2e9165b..b0d64c7 100644 --- a/docs/src/content/docs/challenges/testing/23-harness.md +++ b/docs/src/content/docs/challenges/testing/23-harness.md @@ -2,7 +2,7 @@ title: 🟢 Harness description: Challenge 23 is about testing with component harnesses sidebar: - order: 23 + order: 9 --- :::note @@ -39,7 +39,7 @@ Your PR title must start with Answer:23. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/24-harness-creation.md b/docs/src/content/docs/challenges/testing/24-harness-creation.md index 74df507..c27ce77 100644 --- a/docs/src/content/docs/challenges/testing/24-harness-creation.md +++ b/docs/src/content/docs/challenges/testing/24-harness-creation.md @@ -2,7 +2,7 @@ title: 🟠 Harness Creation description: Challenge 24 is about creating a component harness. sidebar: - order: 24 + order: 112 --- :::note @@ -63,7 +63,7 @@ Your PR title must start with Answer:24. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/28-checkbox.md b/docs/src/content/docs/challenges/testing/28-checkbox.md index 730b503..e157d22 100644 --- a/docs/src/content/docs/challenges/testing/28-checkbox.md +++ b/docs/src/content/docs/challenges/testing/28-checkbox.md @@ -2,26 +2,26 @@ title: 🟢 Checkbox description: Challenge 28 is about testing a simple checkbox sidebar: - order: 28 + order: 10 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #28
## Information -This is the perfect example to get started with `Testing Library`. +This application is very simple. It consists of a checkbox that enables or disables a button. The primary goal of this application is to become familiar with the debug API of Testing Library. Knowing how to debug your tests is a crucial tool you need to have in your toolkit. -You will need to only check if the button gets enabled when clicking on the checkbox +You can find the documentation about debugging in Testing Library [here](https://testing-library.com/docs/dom-testing-library/api-debugging#screenlogtestingplaygroundurl). -You can look into debug function to get full power of `Testing Library` like: +The main functions to remember are as follows: -- logRoles(...); -- screen.debug(); // you can pass a element as input -- screen.logTestingPlaygroundURL(); // you can pass a element as input +- `logRoles(myDOMElement)`: prints out all ARIA roles within the tree of the given DOM element. ARIA roles are the primary selectors you should reach for in the first place. +- `screen.debug()` or `screen.debug(myDOMElement)`: prints the DOM inside the console. +- `screen.logTestingPlaygroundURL()` or `screen.logTestingPlaygroundURL(myDOMElement)`: this function is very powerful. It will create a playground to expose all elements, and you can interact with it to see the selectors you should choose for a DOM element. + +## Statement + +The goal of this challenge is not to submit an answer, but you can if you want. It's more about using the debugging API to play around. These tools will be of great help for the upcoming testing challenges. --- @@ -40,7 +40,7 @@ Your PR title must start with Answer:28. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/29-real-application.md b/docs/src/content/docs/challenges/testing/29-real-application.md index 46e28c3..38aaab6 100644 --- a/docs/src/content/docs/challenges/testing/29-real-application.md +++ b/docs/src/content/docs/challenges/testing/29-real-application.md @@ -2,25 +2,34 @@ title: 🔴 Real-life Application description: Challenge 29 is about testing a real-life application sidebar: - order: 29 + order: 205 --- -:::note -WIP: The following documentation will be reviewed and improved. However, you can still take on the challenge. If you don't understand a certain part, please feel free to reach out or create an issue. -::: -
Challenge #29
-## Statement: +## Information: -I built this more real life application to create more real life test cases. -In this application, you can search for tickets, you can assign or finish them. You can also create new tickets. +This application presents a greater challenge because it closely resembles a real-life application that you might encounter in your day-to-day activities as an Angular developer. What makes it more difficult is the need to handle asynchronous tasks and create appropriate mocks. -This is a very simple application, but it will let you deal with asynchronous task and mocks +The application is a typical todo list application. You can filter tickets, create new ones, assign each ticket, close others, and navigate to the details of each ticket. -The goal of this challenge is to write all test cases of `ticket.store` , `list.component` and `row.component` with Testing Library. +In this challenge, you will write tests for the `ListComponent`, which represents the global view, and the `RowComponent`, which represents a specific ticket. Additionally, you will need to write unit tests for the `TicketStoreService` using Testing Library. _This library allows you to test services effectively._ -You can also do it with cypress. +Handling asynchronous tasks will be particularly challenging. It's important not to introduce any explicit waits in your tests, as this would introduce unnecessary delays. Instead, it's better to look for an element that needs to appear or disappear from the DOM. In this case, the test will naturally wait for the correct period of time, as the waits are already implemented within both libraries. Take advantage of these built-in functionalities to create efficient and reliable tests. + +You can play with it by running : `npx nx serve testing-todos-list`. + +To run Testing Library test suits, you need to run `npx nx test testing-input-output`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks. + +For testing cypress, you will execute your test inside the `child.component.cy.ts` and run `npx nx component-test testing-input-output` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode. + +# Statement + +The goal is to test multiple behaviors of the application describe inside each test files using Testing library and Cypress Component Testing. + +:::note +I have created some `it` blocks but feel free to add more tests if you want. +::: --- @@ -39,7 +48,7 @@ Your PR title must start with Answer:29. ❖ Community Answers ▶︎ Author Answer diff --git a/docs/src/content/docs/challenges/testing/index.mdx b/docs/src/content/docs/challenges/testing/index.mdx new file mode 100644 index 0000000..6149b73 --- /dev/null +++ b/docs/src/content/docs/challenges/testing/index.mdx @@ -0,0 +1,70 @@ +--- +title: Testing +prev: false +next: false +description: Introduction to testing challenges. +sidebar: + order: 1 +--- + +import { LinkCard } from '@astrojs/starlight/components'; + +Testing is a crucial step in building scalable, maintainable, and trustworthy applications. +Testing should never be avoided, even in the face of short deadlines or strong pressure from the product team. +Nowadays, there are numerous awesome tools available that make it easy to test your code and provide a great developer experience. + +In this series of testing exercises, we will learn and master [Testing Library](https://testing-library.com/docs/) and [Cypress Component Testing](https://docs.cypress.io/guides/component-testing/angular/overview) that simplifies DOM manipulation for testing any Angular component. + +The benefits of using Testing Library or Cypress Component Testing are to test your component as a black box. You will only interact with what the user can do on the UI. However, the difference with end-to-end tests is that the backend is mocked, which makes the tests faster and more maintainable. +The goal is to mock as little as possible to test your component at a higher level than unit testing, which will make refactoring easier. +Within a real application, integration tests are the tests you will write the most. Learning how to write them will make your application more robust and more maintainable. + +Here is a series of 8 challenges that you can take in any order. + + + + + + + + + + + + + + + + diff --git a/docs/src/content/docs/challenges/typescript/15-overload-fn.md b/docs/src/content/docs/challenges/typescript/15-overload-fn.md index 9258b62..5a3e630 100644 --- a/docs/src/content/docs/challenges/typescript/15-overload-fn.md +++ b/docs/src/content/docs/challenges/typescript/15-overload-fn.md @@ -2,7 +2,7 @@ title: 🟠 Function Overload description: Challenge 15 is about creating overload functions sidebar: - order: 15 + order: 115 --- :::note @@ -42,7 +42,7 @@ Your PR title must start with Answer:15. ❖ Community Answers ▶︎ Author Answer @@ -51,7 +51,7 @@ Your PR title must start with Answer:15. target="_blank" rel="noopener noreferrer" alt="Function Overload blog article"> - + Blog Post diff --git a/docs/src/content/docs/guides/contribute.md b/docs/src/content/docs/guides/contribute.md index 8c8a5c9..72d5dd3 100644 --- a/docs/src/content/docs/guides/contribute.md +++ b/docs/src/content/docs/guides/contribute.md @@ -5,6 +5,16 @@ sidebar: order: 4 --- -:::note -WIP: -::: +You can contribute to this repository in many ways: + +🔥 Create a new challenge by following the intructions [here](/guides/create-challenge). + +🔥 Answer challenges and submit the results. (guide [here](/guides/resolve-challenge)). + +🔥 Comment on other's solutions by providing constructive and caring feedback. + +🔥 Correct typos or English mistakes within the documentation. + +🔥 File an issue to suggest new challenge ideas or report a bug. + +🔥 Sponsor the project [here](https://github.com/sponsors/tomalaforge) diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index ee9d68f..70e1f86 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -23,8 +23,8 @@ hero: import { Card, CardGrid } from '@astrojs/starlight/components'; - - This repository gathers 35 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript. + + This repository gathers 36 Challenges related to Angular, Nx, RxJS, Ngrx and Typescript. These challenges resolve around real-life issues or specific features to elevate your skills. @@ -56,7 +56,10 @@ import { Card, CardGrid } from '@astrojs/starlight/components'; --- diff --git a/docs/src/styles/custom-css.css b/docs/src/styles/custom-css.css index 06a5bdb..a2d8b82 100644 --- a/docs/src/styles/custom-css.css +++ b/docs/src/styles/custom-css.css @@ -2,18 +2,30 @@ --sl-color-black: #1b1b1d; --sl-color-gray-6: #242526; --sl-color-accent-high: #f10023; - /* --sl-color-black: white; */ --sl-icon-color: #fff; --sl-color-text-invert: #fff; --primary-color: var(--sl-color-bg-nav) !important; - /* --sl-color-text-invert: #212121; */ --cardBgColor: #242526; - /* --sl-font: 'IBM Plex Serif', serif; */ --sl-hue-purple: 41; --sl-color-purple-low: hsl(var(--sl-hue-orange), 39%, 22%); --sl-color-purple: hsl(var(--sl-hue-orange), 82%, 63%); --sl-color-purple-high: hsl(var(--sl-hue-orange), 82%, 87%); + + --color-btn: var(--sl-color-white); + --color-chip: rgb(35, 38, 47); + --color-chip-border: rgba(240, 246, 252, 0.1); +} + +:root[data-theme='light'], +[data-theme='light'] { + --sl-color-accent: #f10023; + --sl-color-accent-high: #f10023; + --sl-icon-color: rgb(35, 38, 47); + + --color-btn: white; + --color-chip: white; + --color-chip-border: rgb(33, 38, 45); } .github-success-btn { @@ -21,7 +33,7 @@ border-radius: 6px; padding: 2px 8px; background-color: rgb(35, 134, 54); - color: var(--sl-color-white); + color: var(-color-btn); } .github-neutral-btn { @@ -38,12 +50,13 @@ } .chip { - border: 1px solid rgba(240, 246, 252, 0.1); + border-width: 1px; + border-style: solid; + border-color: var(--color-chip-border); border-radius: 6px; padding: 2px 8px; - background-color: rgb(33, 38, 45); - color: rgb(201, 209, 217); - fill: rgb(201, 209, 217); + background-color: var(--color-chip); + color: var(--sl-color-text); width: fit-content; } @@ -54,14 +67,14 @@ flex-wrap: wrap; gap: 1.5rem; } + .article-footer > a { border: 1px solid var(--sl-color-accent-high); border-radius: 999rem; padding: 1rem; text-decoration: none; - color: var(--sl-color-gray-2); + color: var(--color-btn); box-shadow: var(--sl-shadow-md); - color: var(--sl-color-white); font-size: var(--sl-text-lg); line-height: var(--sl-line-height-headings); display: flex; @@ -72,14 +85,19 @@ background-color: var(--sl-color-accent-high); } -a.primary { - color: var(--sl-color-white) !important; +a.primary, +a.primary > svg { + color: var(--color-btn) !important; } b { color: var(--sl-color-accent-high); } +.starlight-aside--tip b { + color: var(--sl-color-asides-text-accent); +} + .main-page-footer { margin-top: 2rem !important; font-size: var(--sl-text-sm); @@ -96,3 +114,9 @@ b { starlight-menu-button svg { color: #1c1a1d; } + +.flag { + background-color: white; + padding: 1px 2px; + border-radius: 999px; +} diff --git a/example.README.md b/example.README.md deleted file mode 100644 index 070c246..0000000 --- a/example.README.md +++ /dev/null @@ -1,45 +0,0 @@ - -

{Title of your exercice}

- - - -> Author: {Your name} - - - -## Information - -## Statement - -### Step 1 - -### Step 2 - -### Constraints: - -### Submitting your work - - - -1. Fork the project -2. clone it -3. npm ci - -4. `npx nx serve {project app name}` -5. _...work on it_ -6. Commit your work - -7. Submit a PR with a title beginning with **Answer:{challenge number}** that I will review and other dev can review. - - - -{Project name} - - - - - - diff --git a/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ b/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ index 937cefb..d977f09 100644 --- a/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ +++ b/libs/cli/src/generators/challenge/files/app/src/app/app.component.ts__tmpl__ @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; @Component({ standalone: true, imports: [], - selector: 'lib-root', + selector: 'app-root', template: ``, styles: [''], }) diff --git a/libs/cli/src/generators/challenge/files/docs/__challengeNumber__-__projectName__.md__tmpl__ b/libs/cli/src/generators/challenge/files/docs/__challengeNumber__-__projectName__.md__tmpl__ index 1bd1882..561ed05 100644 --- a/libs/cli/src/generators/challenge/files/docs/__challengeNumber__-__projectName__.md__tmpl__ +++ b/libs/cli/src/generators/challenge/files/docs/__challengeNumber__-__projectName__.md__tmpl__ @@ -2,7 +2,8 @@ title: <%= difficulty %> <%= title %> description: Challenge <%= challengeNumber %> is about ... sidebar: - order: <%= challengeNumber %> + order: <%= order %> + badge: New --- :::note diff --git a/libs/cli/src/generators/challenge/generator.ts b/libs/cli/src/generators/challenge/generator.ts index f5c939f..f829662 100644 --- a/libs/cli/src/generators/challenge/generator.ts +++ b/libs/cli/src/generators/challenge/generator.ts @@ -12,16 +12,44 @@ import { updateJson, } from '@nx/devkit'; import { Linter } from '@nx/linter'; -import { readFile, writeFile } from 'fs/promises'; import { join } from 'path'; import { getProjectDir } from '../../utils/normalize'; import { Schema } from './schema'; +function findPreviousChallengeFilePath(tree, path, number) { + if (tree.isFile(path) && path.startsWith(`${number}-`)) { + return path; + } + + const matchingChild = tree + .children(path) + .find((child) => child.startsWith(`${number}-`)); + + if (matchingChild) { + const fullPath = path + '/' + matchingChild; + return fullPath; + } + + for (const child of tree.children(path)) { + const childPath = path + '/' + child; + const result = findPreviousChallengeFilePath(tree, childPath, number); + if (result) { + return result; + } + } + return null; +} + export async function challengeGenerator(tree: Tree, options: Schema) { const { appDirectory } = getProjectDir(options.name, options.directory); + const difficulty = options.challengeDifficulty; + + // read json file with the total challanges and display order const challengeNumberPath = 'challenge-number.json'; - const challengeNumber = readJsonFile(challengeNumberPath).total; + const challangeNumberJson = readJsonFile(challengeNumberPath); + const challengeNumber = challangeNumberJson.total + 1; + const order = challangeNumberJson[difficulty] + 1; await applicationGenerator(tree, { ...options, @@ -60,7 +88,8 @@ export async function challengeGenerator(tree: Tree, options: Schema) { projectName: names(options.name).name, title: options.title, challengeNumber, - difficulty: options.challengeDifficulty, + difficulty, + order, } ); @@ -70,27 +99,40 @@ export async function challengeGenerator(tree: Tree, options: Schema) { }); } - const readme = await readFile('./README.md', { encoding: 'utf-8' }); + const readme = tree.read('./README.md').toString(); - const readmeRegex = new RegExp(`all ${challengeNumber} challenges`); + const readmeRegex = new RegExp(`all ${challengeNumber - 1} challenges`); const readmeReplace = readme.replace( readmeRegex, - `all ${challengeNumber + 1} challenges` + `all ${challengeNumber} challenges` ); - await writeFile('./README.md', readmeReplace, 'utf-8'); + tree.write('./README.md', readmeReplace); - const docs = await readFile('./docs/src/content/docs/index.mdx', { - encoding: 'utf-8', - }); + const docs = tree.read('./docs/src/content/docs/index.mdx').toString(); - const regex = new RegExp(`${challengeNumber} Challenges`, 'gi'); - const replaced = docs.replace(regex, `${challengeNumber + 1} Challenges`); + const regex = new RegExp(`${challengeNumber - 1} Challenges`, 'gi'); + const replaced = docs.replace(regex, `${challengeNumber} Challenges`); - await writeFile('./docs/src/content/docs/index.mdx', replaced, 'utf-8'); + tree.write('./docs/src/content/docs/index.mdx', replaced); + + const previousChallengeFilePath = findPreviousChallengeFilePath( + tree, + `./docs/src/content/docs/challenges`, + String(challengeNumber - 1) + ); + console.log(`restul`, previousChallengeFilePath); + + const previousChallenge = tree.read(previousChallengeFilePath).toString(); + + tree.write( + previousChallengeFilePath, + previousChallenge.replace(`badge: New`, ``) + ); updateJson(tree, challengeNumberPath, (json) => { - json.total = json.total + 1; + json.total += 1; + json[difficulty] += 1; return json; }); diff --git a/libs/cli/src/generators/readme/generator.ts b/libs/cli/src/generators/readme/generator.ts index db2ddc6..15797bb 100644 --- a/libs/cli/src/generators/readme/generator.ts +++ b/libs/cli/src/generators/readme/generator.ts @@ -1,5 +1,4 @@ import { Tree, formatFiles } from '@nx/devkit'; -import { readFile, writeFile } from 'fs/promises'; const README_FILENAME = 'README.md'; const OMIT = ['memoized', 'projection', 'testing-table', 'testing-forms']; @@ -46,10 +45,10 @@ function findHref(href) { async function rewriteFile(tree: Tree, file: string) { console.log('Current file', file); - const buffer = await readFile(file, { encoding: 'utf-8' }); + const buffer = tree.read(file); const regex = new RegExp(/Answer:(\d+)/); - const match = buffer.match(regex); + const match = buffer.toString().match(regex); if (!match) throw new Error('NO MATCH'); @@ -69,19 +68,19 @@ async function rewriteFile(tree: Tree, file: string) { -2 )}/${pathElts.at(-1)}/`; - const doc = await readFile(docFile, { encoding: 'utf-8' }); + const doc = tree.read(docFile); const regexTitle = new RegExp(/title:\s(🟢|🟠|🔴)\s(.+?)\n/); - const matchTitle = doc.match(regexTitle); + const matchTitle = doc.toString().match(regexTitle); const title = matchTitle[2]; const regexCommand = new RegExp(/npx nx serve\s(.+?)`\s/); - const matchCommand = buffer.match(regexCommand); + const matchCommand = buffer.toString().match(regexCommand); let command = ''; if (!matchCommand) { const regexOldCommand = new RegExp(/nx serve\s(.+?)\*/); - command = buffer.match(regexOldCommand)[1]; + command = buffer.toString().match(regexOldCommand)[1]; } else { command = matchCommand[1]; } @@ -103,12 +102,12 @@ npx nx serve ${command} Challenge documentation is [here](${link}). `; - await writeFile(file, finalText, { encoding: 'utf-8' }); + tree.write(file, finalText); ///**** */ const regexHref = new RegExp(/Answer:${number}. target="_blank" rel="noopener noreferrer" alt="${title} blog article"> - + Blog Post @@ -150,21 +149,21 @@ Your PR title must start with Answer:${number}. } const regexHeader = new RegExp(/([\s\S]*?)\s:::note/); - const header = doc.match(regexHeader)[1]; + const header = doc.toString().match(regexHeader)[1]; console.log('header', header); const regexContent = new RegExp( /Author: Thomas Laforge([\s\S]*?)### Submitting your work/ ); - const matchContent = buffer.match(regexContent); + const matchContent = buffer.toString().match(regexContent); let content = ''; if (!matchContent) { const regexOldContent = new RegExp( /Author: Thomas Laforge([\s\S]*?)## Submitting your work/ ); - content = buffer.match(regexOldContent)[1]; + content = buffer.toString().match(regexOldContent)[1]; } else { content = matchContent[1]; } @@ -184,7 +183,7 @@ ${content} ${footerText} `; - await writeFile(docFile, fullDocText, { encoding: 'utf-8' }); + tree.write(docFile, fullDocText); } export async function readmeGenerator(tree: Tree) { diff --git a/libs/shared/directives/src/index.ts b/libs/shared/directives/src/index.ts index be3fd1e..fd1b226 100644 --- a/libs/shared/directives/src/index.ts +++ b/libs/shared/directives/src/index.ts @@ -1 +1,2 @@ export * from './lib/cd-flashing.directive'; +export { NgForTrackByModule } from './lib/track-by.directive'; diff --git a/libs/shared/directives/src/lib/track-by.directive.ts b/libs/shared/directives/src/lib/track-by.directive.ts new file mode 100644 index 0000000..47ba73a --- /dev/null +++ b/libs/shared/directives/src/lib/track-by.directive.ts @@ -0,0 +1,51 @@ +/* eslint-disable @angular-eslint/directive-selector */ +import { NgFor, NgForOf } from '@angular/common'; +import { + Directive, + Input, + NgIterable, + NgModule, + Provider, + inject, +} from '@angular/core'; + +@Directive({ + selector: '[ngForTrackByProp]', + standalone: true, +}) +export class NgForTrackByPropDirective { + @Input() ngForOf!: NgIterable; + + @Input() + set ngForTrackByProp(ngForTrackBy: keyof T) { + // setter + this.ngFor.ngForTrackBy = (index: number, item: T) => item[ngForTrackBy]; + } + + private ngFor = inject(NgForOf, { self: true }); +} + +@Directive({ + selector: '[ngForTrackById]', + standalone: true, +}) +export class NgForTrackByIdDirective { + @Input() ngForOf!: NgIterable; // 2 + + private ngFor = inject(NgForOf, { self: true }); // 3 + + constructor() { + this.ngFor.ngForTrackBy = (index: number, item: T) => item.id; // 4 + } +} + +export const NgForTrackByDirective: Provider[] = [ + NgForTrackByIdDirective, + NgForTrackByPropDirective, +]; + +@NgModule({ + imports: [NgFor, NgForTrackByDirective], + exports: [NgFor, NgForTrackByDirective], +}) +export class NgForTrackByModule {} diff --git a/package.json b/package.json index 19fcd1d..2087947 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start": "nx serve", "build": "nx build", "test": "nx test", - "prepare": "husky install" + "prepare": "husky install", + "doc:dev": "cd docs && npm run dev" }, "private": true, "dependencies": {