diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 94b4cc74f..27d06982c 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -1,3 +1,3 @@ #!/bin/sh -npm run hook:commit-msg +pnpm exec commitlint --edit $1 diff --git a/.githooks/pre-commit b/.githooks/pre-commit index cd25e400e..9c668ff95 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,3 +1,3 @@ #!/bin/sh -npm run hook:pre-commit \ No newline at end of file +pnpm exec lint-staged diff --git a/apps/docs-app/docs/features/routing/content.md b/apps/docs-app/docs/features/routing/content.md index 62c35070b..52a7b097b 100644 --- a/apps/docs-app/docs/features/routing/content.md +++ b/apps/docs-app/docs/features/routing/content.md @@ -138,7 +138,7 @@ The `injectContent()` function can also be used with an object that contains the This can be useful if, for instance, you have blog posts, as well as a portfolio of project markdown files to be used on the site. -```ts +```treeview src/ └── app/ │ └── pages/ diff --git a/apps/docs-app/docs/features/routing/overview.md b/apps/docs-app/docs/features/routing/overview.md index 0a0901e19..39d70e446 100644 --- a/apps/docs-app/docs/features/routing/overview.md +++ b/apps/docs-app/docs/features/routing/overview.md @@ -22,6 +22,12 @@ There are 5 primary types of routes: These routes can be combined in different ways to build to URLs for navigation. +:::note + +In addition to the 5 primary types of routes, Analog also supports [Redirect Routes](/docs/features/routing/metadata#redirect-routes) and [Content Routes](/docs/features/routing/content). + +::: + ## Index Routes Index routes are defined by using the filename as the route path enclosed in parenthesis. @@ -252,7 +258,7 @@ src/ The above example defines `/login` and `/signup` routes with a shared layout. The parent `src/app/pages/(auth).page.ts` file contains the parent page with a router outlet. -## Catch-all routes +## Catch-all Routes Catch-all routes are defined by using the filename as the route path prefixed with 3 periods enclosed in square brackets. @@ -275,3 +281,41 @@ export default class PageNotFoundComponent {} ``` Catch-all routes can also be defined as nested child routes. + +## Putting It All Together + +For the following file structure: + +```treeview +src/ +└── app/ + └── pages/ + ├── (auth)/ + │ ├── login.page.ts + │ └── signup.page.ts + ├── (marketing)/ + │ ├── about.md + │ └── contact.md + ├── products/ + │ ├── (product-list).page.ts + │ ├── [productId].edit.page.ts + │ └── [productId].page.ts + ├── (auth).page.ts + ├── (home).page.ts + ├── [...not-found].md + └── products.page.ts +``` + +The filesystem-based router will generate the following routes: + +| Path | Page | +| ------------------ | ---------------------------------------------------------------- | +| `/` | `(home).page.ts` | +| `/about` | `(marketing)/about.md` | +| `/contact` | `(marketing)/contact.md` | +| `/login` | `(auth)/login.page.ts` (layout: `(auth).page.ts`) | +| `/signup` | `(auth)/signup.page.ts` (layout: `(auth).page.ts`) | +| `/products` | `products/(product-list).page.ts` (layout: `products.page.ts`) | +| `/products/1` | `products/[productId].page.ts` (layout: `products.page.ts`) | +| `/products/1/edit` | `products/[productId].edit.page.ts` (layout: `products.page.ts`) | +| `/unknown-url` | `[...not-found].md` | diff --git a/package.json b/package.json index b0556571c..d7734dbbd 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,6 @@ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", "contributors:add": "all-contributors add", "contributors:generate": "all-contributors generate", - "hook:commit-msg": "npx --no -- commitlint --edit $1", - "hook:pre-commit": "lint-staged", "prettify": "prettier --write ." }, "engines": { diff --git a/packages/content/src/lib/markdown.component.ts b/packages/content/src/lib/markdown.component.ts index c7bf69ec9..38c92d930 100644 --- a/packages/content/src/lib/markdown.component.ts +++ b/packages/content/src/lib/markdown.component.ts @@ -10,9 +10,10 @@ import { ViewEncapsulation, inject, } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Data } from '@angular/router'; -import { Observable, of } from 'rxjs'; +import { Observable, from, of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { AnchorNavigationDirective } from './anchor-navigation.directive'; @@ -54,15 +55,7 @@ export default class AnalogMarkdownComponent } } - async loadMermaid(mermaidImport: Promise) { - this.mermaid = await mermaidImport; - this.mermaid.default.initialize({ startOnLoad: false }); - // Explicitly running mermaid as ngAfterViewChecked - // has probably already been called - this.zone.runOutsideAngular(() => this.mermaid?.default.run()); - } - - ngOnInit() { + ngOnInit(): void { this.updateContent(); } @@ -87,4 +80,20 @@ export default class AnalogMarkdownComponent this.contentRenderer.enhance(); this.zone.runOutsideAngular(() => this.mermaid?.default.run()); } + + private loadMermaid(mermaidImport: Promise) { + this.zone.runOutsideAngular(() => + // Wrap into an observable to avoid redundant initialization once + // the markdown component is destroyed before the promise is resolved. + from(mermaidImport) + .pipe(takeUntilDestroyed()) + .subscribe((mermaid) => { + this.mermaid = mermaid; + this.mermaid.default.initialize({ startOnLoad: false }); + // Explicitly running mermaid as ngAfterViewChecked + // has probably already been called + this.mermaid?.default.run(); + }) + ); + } } diff --git a/packages/vite-plugin-angular/src/lib/component-resolvers.ts b/packages/vite-plugin-angular/src/lib/component-resolvers.ts index 67904c485..057a2aa8d 100644 --- a/packages/vite-plugin-angular/src/lib/component-resolvers.ts +++ b/packages/vite-plugin-angular/src/lib/component-resolvers.ts @@ -8,10 +8,12 @@ export function hasStyleUrls(code: string) { } interface StyleUrlsCacheEntry { - code: string; + matchedStyleUrls: string; styleUrls: string[]; } +const EMPTY_ARRAY: any[] = []; + export class StyleUrlsResolver { // These resolvers may be called multiple times during the same // compilation for the same files. Caching is required because these @@ -20,32 +22,44 @@ export class StyleUrlsResolver { private readonly styleUrlsCache = new Map(); resolve(code: string, id: string): string[] { - const entry = this.styleUrlsCache.get(id); - if (entry?.code === code) { - return entry.styleUrls; - } + const styleUrlsExecArray = styleUrlsRE.exec(code); - const styleUrlsGroup = styleUrlsRE.exec(code); - - if (Array.isArray(styleUrlsGroup) && styleUrlsGroup[0]) { - const styleUrls = styleUrlsGroup[0].replace( - /(styleUrls|\:|\s|\[|\]|"|')/g, - '' - ); - const styleUrlPaths = styleUrls?.split(',') || []; - - const newEntry = { - code, - styleUrls: styleUrlPaths.map((styleUrlPath) => { - return `${styleUrlPath}|${resolve(dirname(id), styleUrlPath)}`; - }), - }; + if (styleUrlsExecArray === null) { + return EMPTY_ARRAY; + } - this.styleUrlsCache.set(id, newEntry); - return newEntry.styleUrls; + // Given the code is the following: + // @Component({ + // styleUrls: [ + // './app.component.scss' + // ] + // }) + // The `matchedStyleUrls` would result in: `styleUrls: [\n './app.component.scss'\n ]`. + const [matchedStyleUrls] = styleUrlsExecArray; + const entry = this.styleUrlsCache.get(id); + // We're using `matchedStyleUrls` as a key because the code may be changing continuously, + // resulting in the resolver being called multiple times. While the code changes, the + // `styleUrls` may remain constant, which means we should always return the previously + // resolved style URLs. + if (entry?.matchedStyleUrls === matchedStyleUrls) { + return entry.styleUrls; } - return []; + // The `styleUrls` property is an array, which means we may have a list of + // CSS files provided there. Let `matchedStyleUrls` be equal to the following: + // "styleUrls: [\n './app.component.scss',\n '../global.scss'\n ]" + const styleUrlPaths = matchedStyleUrls + .replace(/(styleUrls|\:|\s|\[|\]|"|')/g, '') + // The above replace will result in the following: + // "./app.component.scss,../global.scss" + .split(','); + + const styleUrls = styleUrlPaths.map((styleUrlPath) => { + return `${styleUrlPath}|${resolve(dirname(id), styleUrlPath)}`; + }); + + this.styleUrlsCache.set(matchedStyleUrls, { styleUrls, matchedStyleUrls }); + return styleUrls; } }