diff --git a/apps/blog-app/src/app/blog/models.ts b/apps/blog-app/src/app/blog/models.ts new file mode 100644 index 000000000..94cb21f16 --- /dev/null +++ b/apps/blog-app/src/app/blog/models.ts @@ -0,0 +1,6 @@ +export interface PostAttributes { + title: string; + slug: string; + description: string; + coverImage: string; +} diff --git a/apps/blog-app/src/app/blog/resolvers.ts b/apps/blog-app/src/app/blog/resolvers.ts new file mode 100644 index 000000000..9f2598548 --- /dev/null +++ b/apps/blog-app/src/app/blog/resolvers.ts @@ -0,0 +1,44 @@ +import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router'; +import { MetaTag } from '@analogjs/router'; +import { injectContentFiles } from '@analogjs/content'; +import { PostAttributes } from './models'; + +// temporary +function injectActivePostAttributes( + route: ActivatedRouteSnapshot +): PostAttributes { + return injectContentFiles().find( + (contentFile) => + contentFile.filename === `/src/content/${route.params['slug']}.md` + )!.attributes; +} + +export const postTitleResolver: ResolveFn = (route) => + injectActivePostAttributes(route).title; + +export const postMetaResolver: ResolveFn = (route) => { + const postAttributes = injectActivePostAttributes(route); + + return [ + { + name: 'description', + content: postAttributes.description, + }, + { + name: 'author', + content: 'Analog Team', + }, + { + property: 'og:title', + content: postAttributes.title, + }, + { + property: 'og:description', + content: postAttributes.description, + }, + { + property: 'og:image', + content: postAttributes.coverImage, + }, + ]; +}; diff --git a/apps/blog-app/src/app/routes/blog.ts b/apps/blog-app/src/app/routes/blog.ts index bd2cb42f3..793018e21 100644 --- a/apps/blog-app/src/app/routes/blog.ts +++ b/apps/blog-app/src/app/routes/blog.ts @@ -2,18 +2,20 @@ import { Component } from '@angular/core'; import { RouterLink, RouterOutlet } from '@angular/router'; import { injectContentFiles } from '@analogjs/content'; import { NgFor } from '@angular/common'; +import { RouteMeta } from '@analogjs/router'; +import { PostAttributes } from '../blog/models'; + +export const routeMeta: RouteMeta = { + title: 'Analog Blog', + meta: [{ name: 'description', content: 'Analog Blog Posts' }], +}; -interface MyAttributes { - title: string; - slug: string; -} @Component({ - selector: 'blog', standalone: true, imports: [RouterOutlet, RouterLink, NgFor], template: ` - - {{ attribute.title }} | + + {{ post.attributes.title }} | About @@ -21,7 +23,5 @@ interface MyAttributes { `, }) export default class BlogComponent { - public fileAttributes = injectContentFiles().map( - (file) => file.attributes - ); + readonly posts = injectContentFiles(); } diff --git a/apps/blog-app/src/app/routes/blog/[slug].ts b/apps/blog-app/src/app/routes/blog/[slug].ts index 36b9f3845..6b4781313 100644 --- a/apps/blog-app/src/app/routes/blog/[slug].ts +++ b/apps/blog-app/src/app/routes/blog/[slug].ts @@ -1,19 +1,25 @@ import { injectContent, MarkdownComponent } from '@analogjs/content'; import { AsyncPipe, NgIf } from '@angular/common'; import { Component } from '@angular/core'; -import { BlogAttributes } from '../../../lib/blog-attributes'; +import { RouteMeta } from '@analogjs/router'; +import { PostAttributes } from '../../blog/models'; +import { postMetaResolver, postTitleResolver } from '../../blog/resolvers'; + +export const routeMeta: RouteMeta = { + title: postTitleResolver, + meta: postMetaResolver, +}; @Component({ - selector: 'blog-post', standalone: true, imports: [MarkdownComponent, AsyncPipe, NgIf], template: ` - -

{{ cf.attributes.title }}

- + +

{{ post.attributes.title }}

+
`, }) export default class BlogPostComponent { - public contentFile$ = injectContent(); + readonly post$ = injectContent(); } diff --git a/apps/blog-app/src/content/2022-12-27-my-first-post.md b/apps/blog-app/src/content/2022-12-27-my-first-post.md index 744ac6a8d..9f6432d5b 100644 --- a/apps/blog-app/src/content/2022-12-27-my-first-post.md +++ b/apps/blog-app/src/content/2022-12-27-my-first-post.md @@ -1,6 +1,8 @@ --- title: My First Post slug: 2022-12-27-my-first-post +description: My First Post Description +coverImage: https://images.unsplash.com/photo-1493612276216-ee3925520721?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=464&q=80 --- Hello diff --git a/apps/blog-app/src/content/2022-12-31-my-second-post.md b/apps/blog-app/src/content/2022-12-31-my-second-post.md index 0a9ede668..eedd42479 100644 --- a/apps/blog-app/src/content/2022-12-31-my-second-post.md +++ b/apps/blog-app/src/content/2022-12-31-my-second-post.md @@ -1,6 +1,8 @@ --- title: My Second Post slug: 2022-12-31-my-second-post +description: My Second Post Description +coverImage: https://images.unsplash.com/photo-1481627834876-b7833e8f5570?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=928&q=80 --- - [Home](/) diff --git a/apps/blog-app/src/lib/blog-attributes.ts b/apps/blog-app/src/lib/blog-attributes.ts deleted file mode 100644 index 66babdff2..000000000 --- a/apps/blog-app/src/lib/blog-attributes.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface BlogAttributes { - title: string; - slug: string; -} diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index db4600528..f12e02ba5 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -5,4 +5,5 @@ export { injectRouter, } from './lib/define-route'; export { RouteMeta } from './lib/models'; -export { provideFileRouter } from './lib/provide-file-routes'; +export { provideFileRouter } from './lib/provide-file-router'; +export { MetaTag } from './lib/meta-tags'; diff --git a/packages/router/src/lib/meta-tags.spec.ts b/packages/router/src/lib/meta-tags.spec.ts new file mode 100644 index 000000000..34475f197 --- /dev/null +++ b/packages/router/src/lib/meta-tags.spec.ts @@ -0,0 +1,226 @@ +import { Component, ENVIRONMENT_INITIALIZER } from '@angular/core'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { DOCUMENT } from '@angular/common'; +import { Route, Router, RouterOutlet } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { map, timer } from 'rxjs'; +import { + MetaTag, + ROUTE_META_TAGS_KEY, + updateMetaTagsOnRouteChange, +} from './meta-tags'; + +describe('updateMetaTagsOnRouteChange', () => { + function setup() { + @Component({ + standalone: true, + imports: [RouterOutlet], + template: '', + }) + class TestComponent {} + + const parentMetaTagValues = { + charset: 'utf-8', + httpEquivRefresh: '1000', + description: 'Parent Description', + keywords: 'Analog, Angular', + ogTitle: 'Parent Og:Title', + ogImage: 'https://example.com', + }; + + const childMetaTagValues = { + charset: 'ascii', + httpEquivRefresh: '3000', + httpEquivContentSec: "default-src 'self'", + description: 'Child Description', + author: 'Analog Team', + ogTitle: 'Child Og:Title', + ogDescription: 'Child Og:Description', + }; + + const routes: Route[] = [ + { + path: '', + component: TestComponent, + data: { + [ROUTE_META_TAGS_KEY]: [ + { charset: parentMetaTagValues.charset }, + { + httpEquiv: 'refresh', + content: parentMetaTagValues.httpEquivRefresh, + }, + { name: 'description', content: parentMetaTagValues.description }, + { name: 'keywords', content: parentMetaTagValues.keywords }, + { property: 'og:title', content: parentMetaTagValues.ogTitle }, + { property: 'og:image', content: parentMetaTagValues.ogImage }, + ] as MetaTag[], + }, + children: [ + { + path: 'child', + component: TestComponent, + resolve: { + [ROUTE_META_TAGS_KEY]: () => + timer(1000).pipe( + map( + () => + [ + { charset: childMetaTagValues.charset }, + { + httpEquiv: 'refresh', + content: childMetaTagValues.httpEquivRefresh, + }, + { + httpEquiv: 'content-security-policy', + content: childMetaTagValues.httpEquivContentSec, + }, + { + name: 'description', + content: childMetaTagValues.description, + }, + { name: 'author', content: childMetaTagValues.author }, + { + property: 'og:title', + content: childMetaTagValues.ogTitle, + }, + { + property: 'og:description', + content: childMetaTagValues.ogDescription, + }, + ] as MetaTag[] + ) + ), + }, + }, + ], + }, + ]; + + TestBed.configureTestingModule({ + imports: [RouterTestingModule.withRoutes(routes)], + providers: [ + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => updateMetaTagsOnRouteChange(), + }, + ], + }); + + const router = TestBed.inject(Router); + const document = TestBed.inject(DOCUMENT); + + const getMetaElements = () => ({ + charset: document.querySelector('meta[charset]') as HTMLMetaElement, + httpEquivRefresh: document.querySelector( + 'meta[http-equiv="refresh"]' + ) as HTMLMetaElement, + httpEquivContentSec: document.querySelector( + 'meta[http-equiv="content-security-policy"]' + ) as HTMLMetaElement, + description: document.querySelector( + 'meta[name="description"]' + ) as HTMLMetaElement, + keywords: document.querySelector( + 'meta[name="keywords"]' + ) as HTMLMetaElement, + author: document.querySelector('meta[name="author"]') as HTMLMetaElement, + ogTitle: document.querySelector( + 'meta[property="og:title"]' + ) as HTMLMetaElement, + ogDescription: document.querySelector( + 'meta[property="og:description"]' + ) as HTMLMetaElement, + ogImage: document.querySelector( + 'meta[property="og:image"]' + ) as HTMLMetaElement, + }); + + return { router, getMetaElements, parentMetaTagValues, childMetaTagValues }; + } + + it('adds meta tags on initial navigation', fakeAsync(() => { + const { router, getMetaElements, parentMetaTagValues } = setup(); + + router.navigateByUrl('/'); + tick(); + + const metaElements = getMetaElements(); + expect(metaElements.charset.getAttribute('charset')).toBe( + parentMetaTagValues.charset + ); + expect(metaElements.httpEquivRefresh.content).toBe( + parentMetaTagValues.httpEquivRefresh + ); + expect(metaElements.description.content).toBe( + parentMetaTagValues.description + ); + expect(metaElements.keywords.content).toBe(parentMetaTagValues.keywords); + expect(metaElements.ogTitle.content).toBe(parentMetaTagValues.ogTitle); + expect(metaElements.ogImage.content).toBe(parentMetaTagValues.ogImage); + })); + + it('merges parent and child meta tags on child route navigation', fakeAsync(() => { + const { router, getMetaElements, parentMetaTagValues, childMetaTagValues } = + setup(); + + router.navigateByUrl('/child'); + // child meta tags are resolved after 1s + tick(1000); + + const metaElements = getMetaElements(); + expect(metaElements.charset.getAttribute('charset')).toBe( + childMetaTagValues.charset + ); + expect(metaElements.httpEquivRefresh.content).toBe( + childMetaTagValues.httpEquivRefresh + ); + expect(metaElements.httpEquivContentSec.content).toBe( + childMetaTagValues.httpEquivContentSec + ); + expect(metaElements.description.content).toBe( + childMetaTagValues.description + ); + expect(metaElements.author.content).toBe(childMetaTagValues.author); + expect(metaElements.ogTitle.content).toBe(childMetaTagValues.ogTitle); + expect(metaElements.ogDescription.content).toBe( + childMetaTagValues.ogDescription + ); + // meta tags inherited from parent route + expect(metaElements.keywords.content).toBe(parentMetaTagValues.keywords); + expect(metaElements.ogImage.content).toBe(parentMetaTagValues.ogImage); + })); + + it('lefts over meta tags from the previous route that are not changed', fakeAsync(() => { + const { router, getMetaElements, parentMetaTagValues, childMetaTagValues } = + setup(); + + router.navigateByUrl('/child'); + tick(1000); + + router.navigateByUrl('/'); + tick(); + + const metaElements = getMetaElements(); + expect(metaElements.charset.getAttribute('charset')).toBe( + parentMetaTagValues.charset + ); + expect(metaElements.description.content).toBe( + parentMetaTagValues.description + ); + expect(metaElements.keywords.content).toBe(parentMetaTagValues.keywords); + expect(metaElements.httpEquivRefresh.content).toBe( + parentMetaTagValues.httpEquivRefresh + ); + expect(metaElements.ogTitle.content).toBe(parentMetaTagValues.ogTitle); + expect(metaElements.ogImage.content).toBe(parentMetaTagValues.ogImage); + // meta tags that are not changed + expect(metaElements.httpEquivContentSec.content).toBe( + childMetaTagValues.httpEquivContentSec + ); + expect(metaElements.author.content).toBe(childMetaTagValues.author); + expect(metaElements.ogDescription.content).toBe( + childMetaTagValues.ogDescription + ); + })); +}); diff --git a/packages/router/src/lib/meta-tags.ts b/packages/router/src/lib/meta-tags.ts new file mode 100644 index 000000000..40fc29122 --- /dev/null +++ b/packages/router/src/lib/meta-tags.ts @@ -0,0 +1,94 @@ +import { inject } from '@angular/core'; +import { Meta, MetaDefinition as NgMetaTag } from '@angular/platform-browser'; +import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; + +export const ROUTE_META_TAGS_KEY = Symbol( + '@analogjs/router Route Meta Tags Key' +); + +const CHARSET_KEY = 'charset'; +const HTTP_EQUIV_KEY = 'httpEquiv'; +// httpEquiv selector key needs to be in kebab case format +const HTTP_EQUIV_SELECTOR_KEY = 'http-equiv'; +const NAME_KEY = 'name'; +const PROPERTY_KEY = 'property'; +const CONTENT_KEY = 'content'; + +export type MetaTag = + | (CharsetMetaTag & ExcludeRestMetaTagKeys) + | (HttpEquivMetaTag & ExcludeRestMetaTagKeys) + | (NameMetaTag & ExcludeRestMetaTagKeys) + | (PropertyMetaTag & ExcludeRestMetaTagKeys); + +type CharsetMetaTag = { [CHARSET_KEY]: string }; +type HttpEquivMetaTag = { [HTTP_EQUIV_KEY]: string; [CONTENT_KEY]: string }; +type NameMetaTag = { [NAME_KEY]: string; [CONTENT_KEY]: string }; +type PropertyMetaTag = { [PROPERTY_KEY]: string; [CONTENT_KEY]: string }; + +type MetaTagKey = + | typeof CHARSET_KEY + | typeof HTTP_EQUIV_KEY + | typeof NAME_KEY + | typeof PROPERTY_KEY; +type ExcludeRestMetaTagKeys = { + [K in Exclude]?: never; +}; + +type MetaTagSelector = + | typeof CHARSET_KEY + | `${ + | typeof HTTP_EQUIV_SELECTOR_KEY + | typeof NAME_KEY + | typeof PROPERTY_KEY}="${string}"`; +type MetaTagMap = Record; + +export function updateMetaTagsOnRouteChange(): void { + const router = inject(Router); + const metaService = inject(Meta); + + router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => { + const metaTagMap = getMetaTagMap(router.routerState.snapshot.root); + + for (const metaTagSelector in metaTagMap) { + const metaTag = metaTagMap[ + metaTagSelector as MetaTagSelector + ] as NgMetaTag; + metaService.updateTag(metaTag, metaTagSelector); + } + }); +} + +function getMetaTagMap(route: ActivatedRouteSnapshot): MetaTagMap { + const metaTagMap = {} as MetaTagMap; + let currentRoute: ActivatedRouteSnapshot | null = route; + + while (currentRoute) { + const metaTags: MetaTag[] = currentRoute.data[ROUTE_META_TAGS_KEY] ?? []; + for (const metaTag of metaTags) { + metaTagMap[getMetaTagSelector(metaTag)] = metaTag; + } + + currentRoute = currentRoute.firstChild; + } + + return metaTagMap; +} + +function getMetaTagSelector(metaTag: MetaTag): MetaTagSelector { + if (metaTag.name) { + return `${NAME_KEY}="${metaTag.name}"`; + } + + if (metaTag.property) { + return `${PROPERTY_KEY}="${metaTag.property}"`; + } + + if (metaTag.httpEquiv) { + return `${HTTP_EQUIV_SELECTOR_KEY}="${metaTag.httpEquiv}"`; + } + + return CHARSET_KEY; +} diff --git a/packages/router/src/lib/models.ts b/packages/router/src/lib/models.ts index 3ec41e0e9..cda3fdd68 100644 --- a/packages/router/src/lib/models.ts +++ b/packages/router/src/lib/models.ts @@ -9,12 +9,11 @@ import { } from '@angular/router'; import { defineRouteMeta } from './define-route'; +import { MetaTag } from './meta-tags'; type OmittedRouteProps = | 'path' - | 'pathMatch' | 'matcher' - | 'redirectTo' | 'component' | 'loadComponent' | 'children' @@ -22,16 +21,20 @@ type OmittedRouteProps = | 'canLoad' | 'outlet'; -interface DefaultRouteMeta extends Omit { +export type RouteConfig = Omit; + +export interface DefaultRouteMeta + extends Omit { canActivate?: CanActivateFn[]; canActivateChild?: CanActivateChildFn[]; canDeactivate?: CanDeactivateFn[]; canMatch?: CanMatchFn[]; resolve?: { [key: string | symbol]: ResolveFn }; title?: string | ResolveFn; + meta?: MetaTag[] | ResolveFn; } -interface RedirectRouteMeta { +export interface RedirectRouteMeta { redirectTo: string; pathMatch?: Route['pathMatch']; } diff --git a/packages/router/src/lib/provide-file-router.ts b/packages/router/src/lib/provide-file-router.ts new file mode 100644 index 000000000..af9c32224 --- /dev/null +++ b/packages/router/src/lib/provide-file-router.ts @@ -0,0 +1,37 @@ +import { + ENVIRONMENT_INITIALIZER, + EnvironmentProviders, + makeEnvironmentProviders, + Provider, +} from '@angular/core'; +import { provideRouter, RouterFeatures } from '@angular/router'; + +import { routes } from './routes'; +import { updateMetaTagsOnRouteChange } from './meta-tags'; + +/** + * Sets up providers for the Angular router, and registers + * file-based routes. Additional features can be provided + * to further configure the behavior of the router. + * + * @param features + * @returns Providers and features to configure the router with routes + */ +export function provideFileRouter( + ...features: RouterFeatures[] +): EnvironmentProviders { + return makeEnvironmentProviders([ + // TODO: remove type casting after Angular >=15.1.1 upgrade + // https://github.com/angular/angular/pull/48720 + ( + provideRouter(routes, ...features) as unknown as { + ɵproviders: Provider[]; + } + ).ɵproviders, + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue: () => updateMetaTagsOnRouteChange(), + }, + ]); +} diff --git a/packages/router/src/lib/provide-file-routes.ts b/packages/router/src/lib/provide-file-routes.ts deleted file mode 100644 index af425237f..000000000 --- a/packages/router/src/lib/provide-file-routes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { provideRouter, RouterFeatures } from '@angular/router'; -import { routes } from './routes'; - -/** - * Sets up providers for the Angular router, and registers - * file-based routes. Additional features can be provided - * to further configure the behavior of the router. - * - * @param features - * @returns Providers and features to configure the router with routes - */ -export const provideFileRouter = (...features: RouterFeatures[]) => { - return provideRouter(routes, ...features); -}; diff --git a/packages/router/src/lib/route-config.ts b/packages/router/src/lib/route-config.ts new file mode 100644 index 000000000..f2cf93d1f --- /dev/null +++ b/packages/router/src/lib/route-config.ts @@ -0,0 +1,31 @@ +import { RedirectRouteMeta, RouteConfig, RouteMeta } from './models'; +import { ROUTE_META_TAGS_KEY } from './meta-tags'; + +export function toRouteConfig(routeMeta: RouteMeta | undefined): RouteConfig { + if (!routeMeta) { + return {}; + } + + if (isRedirectRouteMeta(routeMeta)) { + return routeMeta; + } + + const { meta, ...routeConfig } = routeMeta; + + if (Array.isArray(meta)) { + routeConfig.data = { ...routeConfig.data, [ROUTE_META_TAGS_KEY]: meta }; + } else if (typeof meta === 'function') { + routeConfig.resolve = { + ...routeConfig.resolve, + [ROUTE_META_TAGS_KEY]: meta, + }; + } + + return routeConfig; +} + +function isRedirectRouteMeta( + routeMeta: RouteMeta +): routeMeta is RedirectRouteMeta { + return !!routeMeta.redirectTo; +} diff --git a/packages/router/src/lib/routes.spec.ts b/packages/router/src/lib/routes.spec.ts index 033401fda..800a577a7 100644 --- a/packages/router/src/lib/routes.spec.ts +++ b/packages/router/src/lib/routes.spec.ts @@ -1,6 +1,8 @@ import { Route } from '@angular/router'; -import { RouteExport } from './models'; +import { of } from 'rxjs'; +import { RouteExport, RouteMeta } from './models'; import { getRoutes } from './routes'; +import { ROUTE_META_TAGS_KEY } from './meta-tags'; type Files = Record Promise>; type ModuleRoute = Route & { @@ -315,4 +317,66 @@ describe('routes', () => { expect(innerRoute.component).toBe(RouteComponent); }); }); + + describe('a route with meta tags', () => { + async function setup(routeMeta: RouteMeta) { + const files: Files = { + '/app/routes/index.ts': () => + Promise.resolve({ default: RouteComponent, routeMeta }), + }; + const moduleRoute = getRoutes(files)[0] as ModuleRoute; + const resolvedRoutes = (await moduleRoute.loadChildren?.()) as Route[]; + + return { resolvedRoute: resolvedRoutes[0] }; + } + + it('should add meta tags to data dictionary when they are defined as array', async () => { + const routeMeta: RouteMeta = { + data: { foo: 'bar' }, + resolve: { x: () => of('y') }, + meta: [ + { charset: 'utf-8' }, + { + name: 'description', + content: 'Books Description', + }, + ], + }; + const { resolvedRoute } = await setup(routeMeta); + + expect(resolvedRoute.data).toEqual({ + ...routeMeta.data, + [ROUTE_META_TAGS_KEY]: routeMeta.meta, + }); + // routeMeta.data should not be mutated + expect(routeMeta.data).not.toBe(resolvedRoute.data); + // routeMeta.resolve should not be changed + expect(resolvedRoute.resolve).toBe(routeMeta.resolve); + }); + + it('should add meta tags to resolve dictionary when they are defined as resolver', async () => { + const routeMeta: RouteMeta = { + resolve: { foo: () => of('bar') }, + data: { x: 1, y: 2 }, + meta: () => + of([ + { charset: 'utf-8' }, + { + name: 'description', + content: 'Books Description', + }, + ]), + }; + const { resolvedRoute } = await setup(routeMeta); + + expect(resolvedRoute.resolve).toEqual({ + ...routeMeta.resolve, + [ROUTE_META_TAGS_KEY]: routeMeta.meta, + }); + // routeMeta.resolve should not be mutated + expect(routeMeta.resolve).not.toBe(resolvedRoute.resolve); + // routeMeta.data should not be changed + expect(resolvedRoute.data).toBe(routeMeta.data); + }); + }); }); diff --git a/packages/router/src/lib/routes.ts b/packages/router/src/lib/routes.ts index 01afeadfa..07fc3e84e 100644 --- a/packages/router/src/lib/routes.ts +++ b/packages/router/src/lib/routes.ts @@ -2,7 +2,8 @@ import type { Route } from '@angular/router'; -import { RouteExport } from './models'; +import { RouteExport, RouteMeta } from './models'; +import { toRouteConfig } from './route-config'; const FILES = import.meta.glob([ '/app/routes/**/*.ts', @@ -79,7 +80,7 @@ export function getRoutes(files: Record Promise>) { { path: '', component: m.default, - ...m.routeMeta, + ...toRouteConfig(m.routeMeta as RouteMeta | undefined), }, ]), }; @@ -109,7 +110,7 @@ export function getRoutes(files: Record Promise>) { { path: '', component: m.default, - ...m.routeMeta, + ...toRouteConfig(m.routeMeta as RouteMeta | undefined), }, ]), }); @@ -131,7 +132,7 @@ export function getRoutes(files: Record Promise>) { { path: '', component: m.default, - ...m.routeMeta, + ...toRouteConfig(m.routeMeta as RouteMeta | undefined), }, ]), }); @@ -146,7 +147,7 @@ export function getRoutes(files: Record Promise>) { { path: '', children: parent._children, - ...m.routeMeta, + ...toRouteConfig(m.routeMeta as RouteMeta | undefined), }, ]; }); diff --git a/packages/router/src/test-setup.ts b/packages/router/src/test-setup.ts index 88e9a8cf4..f1bfb8ca6 100644 --- a/packages/router/src/test-setup.ts +++ b/packages/router/src/test-setup.ts @@ -1,2 +1,16 @@ import '@analogjs/vite-plugin-angular/setup-vitest'; import '@angular/compiler'; + +/** + * Initialize TestBed for all tests inside of router + */ +import { TestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +TestBed.initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); diff --git a/packages/router/vite.config.ts b/packages/router/vite.config.ts index 00f36aa0e..d7e8005d8 100644 --- a/packages/router/vite.config.ts +++ b/packages/router/vite.config.ts @@ -2,22 +2,18 @@ import { defineConfig } from 'vite'; import { offsetFromRoot } from '@nrwl/devkit'; -import angular from '@analogjs/vite-plugin-angular'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { return { root: 'src', - // plugins: [angular()], test: { globals: true, environment: 'jsdom', setupFiles: ['src/test-setup.ts'], include: ['**/*.spec.ts'], cache: { - dir: `${offsetFromRoot( - 'packages/astro-angular/src' - )}/node_modules/.vitest`, + dir: `${offsetFromRoot('packages/router/src')}/node_modules/.vitest`, }, }, define: {