UNPKG

@analogjs/router

Version:

Filesystem-based routing for Angular

808 lines (787 loc) 30.9 kB
import * as i0 from '@angular/core'; import { inject, PLATFORM_ID, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, Injector, makeStateKey, TransferState, input, output, Directive, InjectionToken, signal, effect, ChangeDetectionStrategy, Component } from '@angular/core'; import { HttpClient, HttpHeaders, ɵHTTP_ROOT_INTERCEPTOR_FNS as _HTTP_ROOT_INTERCEPTOR_FNS, HttpResponse, HttpRequest } from '@angular/common/http'; import { firstValueFrom, map, from, of, throwError, catchError } from 'rxjs'; import { Meta, DomSanitizer } from '@angular/platform-browser'; import { Router, NavigationEnd, ActivatedRoute, provideRouter, ROUTES } from '@angular/router'; import { filter } from 'rxjs/operators'; import { injectAPIPrefix, injectBaseURL, injectRequest, API_PREFIX } from '@analogjs/router/tokens'; import { isPlatformServer } from '@angular/common'; 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'; function updateMetaTagsOnRouteChange() { 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]; metaService.updateTag(metaTag, metaTagSelector); } }); } function getMetaTagMap(route) { const metaTagMap = {}; let currentRoute = route; while (currentRoute) { const metaTags = currentRoute.data[ROUTE_META_TAGS_KEY] ?? []; for (const metaTag of metaTags) { metaTagMap[getMetaTagSelector(metaTag)] = metaTag; } currentRoute = currentRoute.firstChild; } return metaTagMap; } function getMetaTagSelector(metaTag) { 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; } const ANALOG_META_KEY = Symbol('@analogjs/router Analog Route Metadata Key'); /** * This variable reference is replaced with a glob of all route endpoints. */ let ANALOG_PAGE_ENDPOINTS = {}; function injectRouteEndpointURL(route) { const routeConfig = route.routeConfig; const apiPrefix = injectAPIPrefix(); const baseUrl = injectBaseURL(); const { queryParams, fragment: hash, params, parent } = route; const segment = parent?.url.map((segment) => segment.path).join('/') || ''; const url = new URL('', import.meta.env['VITE_ANALOG_PUBLIC_BASE_URL'] || baseUrl || (typeof window !== 'undefined' && window.location.origin ? window.location.origin : '')); url.pathname = `${url.pathname.endsWith('/') ? url.pathname : url.pathname + '/'}${apiPrefix}/_analog${routeConfig[ANALOG_META_KEY].endpoint}`; url.search = `${new URLSearchParams(queryParams).toString()}`; url.hash = hash ?? ''; Object.keys(params).forEach((param) => { url.pathname = url.pathname.replace(`[${param}]`, params[param]); }); url.pathname = url.pathname.replace('**', segment); return url; } function toRouteConfig(routeMeta) { if (routeMeta && isRedirectRouteMeta(routeMeta)) { return routeMeta; } let { 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, }; } if (!routeConfig) { routeConfig = {}; } routeConfig.runGuardsAndResolvers = routeConfig.runGuardsAndResolvers ?? 'paramsOrQueryParamsChange'; routeConfig.resolve = { ...routeConfig.resolve, load: async (route) => { const routeConfig = route.routeConfig; if (ANALOG_PAGE_ENDPOINTS[routeConfig[ANALOG_META_KEY].endpointKey]) { const http = inject(HttpClient); const url = injectRouteEndpointURL(route); if (!!import.meta.env['VITE_ANALOG_PUBLIC_BASE_URL'] && globalThis.$fetch) { return globalThis.$fetch(url.pathname); } return firstValueFrom(http.get(`${url.href}`)); } return {}; }, }; return routeConfig; } function isRedirectRouteMeta(routeMeta) { return !!routeMeta.redirectTo; } // The Zone is currently enabled by default, so we wouldn't need this check. // However, leaving this open space will be useful if zone.js becomes optional // in the future. This means we won't have to modify the current code, and it will // continue to work seamlessly. const isNgZoneEnabled = typeof Zone !== 'undefined' && !!Zone.root; function toMarkdownModule(markdownFileFactory) { return async () => { const createLoader = () => Promise.all([import('@analogjs/content'), markdownFileFactory()]); const [{ parseRawContentFile, MarkdownRouteComponent, ContentRenderer }, markdownFile,] = await (isNgZoneEnabled ? // We are not able to use `runOutsideAngular` because we are not inside // an injection context to retrieve the `NgZone` instance. // The `Zone.root.run` is required when the code is running in the // browser since asynchronous tasks being scheduled in the current context // are a reason for unnecessary change detection cycles. Zone.root.run(createLoader) : createLoader()); const { content, attributes } = parseRawContentFile(markdownFile); const { title, meta } = attributes; return { default: MarkdownRouteComponent, routeMeta: { data: { _analogContent: content }, title, meta, resolve: { renderedAnalogContent: async () => { const contentRenderer = inject(ContentRenderer); return contentRenderer.render(content); }, }, }, }; }; } const ENDPOINT_EXTENSION = '.server.ts'; const APP_DIR = 'src/app'; /** * This variable reference is replaced with a glob of all page routes. */ let ANALOG_ROUTE_FILES = {}; /** * This variable reference is replaced with a glob of all content routes. */ let ANALOG_CONTENT_ROUTE_FILES = {}; /** * A function used to parse list of files and create configuration of routes. * * @param files * @returns Array of routes */ function createRoutes(files, debug = false) { const filenames = Object.keys(files); if (filenames.length === 0) { return []; } // map filenames to raw routes and group them by level const rawRoutesByLevelMap = filenames.reduce((acc, filename) => { const rawPath = toRawPath(filename); const rawSegments = rawPath.split('/'); // nesting level starts at 0 // rawPath: /products => level: 0 // rawPath: /products/:id => level: 1 const level = rawSegments.length - 1; const rawSegment = rawSegments[level]; const ancestorRawSegments = rawSegments.slice(0, level); return { ...acc, [level]: { ...acc[level], [rawPath]: { filename, rawSegment, ancestorRawSegments, segment: toSegment(rawSegment), level, children: [], }, }, }; }, {}); const allLevels = Object.keys(rawRoutesByLevelMap).map(Number); const maxLevel = Math.max(...allLevels); // add each raw route to its parent's children array for (let level = maxLevel; level > 0; level--) { const rawRoutesMap = rawRoutesByLevelMap[level]; const rawPaths = Object.keys(rawRoutesMap); for (const rawPath of rawPaths) { const rawRoute = rawRoutesMap[rawPath]; const parentRawPath = rawRoute.ancestorRawSegments.join('/'); const parentRawSegmentIndex = rawRoute.ancestorRawSegments.length - 1; const parentRawSegment = rawRoute.ancestorRawSegments[parentRawSegmentIndex]; // create the parent level and/or raw route if it does not exist // parent route won't exist for nested routes that don't have a layout route rawRoutesByLevelMap[level - 1] ||= {}; rawRoutesByLevelMap[level - 1][parentRawPath] ||= { filename: null, rawSegment: parentRawSegment, ancestorRawSegments: rawRoute.ancestorRawSegments.slice(0, parentRawSegmentIndex), segment: toSegment(parentRawSegment), level: level - 1, children: [], }; rawRoutesByLevelMap[level - 1][parentRawPath].children.push(rawRoute); } } // only take raw routes from the root level // since they already contain nested routes as their children const rootRawRoutesMap = rawRoutesByLevelMap[0]; const rawRoutes = Object.keys(rootRawRoutesMap).map((segment) => rootRawRoutesMap[segment]); sortRawRoutes(rawRoutes); return toRoutes(rawRoutes, files, debug); } function toRawPath(filename) { return filename .replace( // convert to relative path and remove file extension /^(?:[a-zA-Z]:[\\/])?(.*?)[\\/](?:routes|pages)[\\/]|(?:[\\/](?:app[\\/](?:routes|pages)[\\/]))|(\.page\.(js|ts|analog|ag)$)|(\.(ts|md|analog|ag)$)/g, '') .replace(/\[\.{3}.+\]/, '**') // [...not-found] => ** .replace(/\[([^\]]+)\]/g, ':$1'); // [id] => :id } function toSegment(rawSegment) { return rawSegment .replace(/index|\(.*?\)/g, '') // replace named empty segments .replace(/\.|\/+/g, '/') // replace dots with slashes and remove redundant slashes .replace(/^\/+|\/+$/g, ''); // remove trailing slashes } function toRoutes(rawRoutes, files, debug = false) { const routes = []; for (const rawRoute of rawRoutes) { const children = rawRoute.children.length > 0 ? toRoutes(rawRoute.children, files, debug) : undefined; let module = undefined; let analogMeta = undefined; if (rawRoute.filename) { const isMarkdownFile = rawRoute.filename.endsWith('.md'); if (!debug) { module = isMarkdownFile ? toMarkdownModule(files[rawRoute.filename]) : files[rawRoute.filename]; } const endpointKey = rawRoute.filename.replace(/\.page\.(ts|analog|ag)$/, ENDPOINT_EXTENSION); // get endpoint path const rawEndpoint = rawRoute.filename .replace(/\.page\.(ts|analog|ag)$/, '') .replace(/\[\.{3}.+\]/, '**') // [...not-found] => ** .replace(/^(.*?)\/pages/, '/pages'); // replace periods, remove (index) paths const endpoint = (rawEndpoint || '') .replace(/\./g, '/') .replace(/\/\((.*?)\)$/, '/-$1-'); analogMeta = { endpoint, endpointKey, }; } const route = module ? { path: rawRoute.segment, loadChildren: () => module().then((m) => { if (import.meta.env.DEV) { const hasModuleDefault = !!m.default; const hasRedirect = !!m.routeMeta?.redirectTo; if (!hasModuleDefault && !hasRedirect) { console.warn(`[Analog] Missing default export at ${rawRoute.filename}`); } } return [ { path: '', component: m.default, ...toRouteConfig(m.routeMeta), children, [ANALOG_META_KEY]: analogMeta, }, ]; }), } : { path: rawRoute.segment, ...(debug ? { filename: rawRoute.filename ? rawRoute.filename : undefined, isLayout: children && children.length > 0 ? true : false, } : {}), children, }; routes.push(route); } return routes; } function sortRawRoutes(rawRoutes) { rawRoutes.sort((a, b) => { let segmentA = deprioritizeSegment(a.segment); let segmentB = deprioritizeSegment(b.segment); // prioritize routes with fewer children if (a.children.length > b.children.length) { segmentA = `~${segmentA}`; } else if (a.children.length < b.children.length) { segmentB = `~${segmentB}`; } return segmentA > segmentB ? 1 : -1; }); for (const rawRoute of rawRoutes) { sortRawRoutes(rawRoute.children); } } function deprioritizeSegment(segment) { // deprioritize param and wildcard segments return segment.replace(':', '~~').replace('**', '~~~~'); } const routes = createRoutes({ ...ANALOG_ROUTE_FILES, ...ANALOG_CONTENT_ROUTE_FILES, }); /** * @deprecated Use `RouteMeta` type instead. * For more info see: https://github.com/analogjs/analog/issues/223 * * Defines additional route config metadata. This * object is merged into the route config with * the predefined file-based route. * * @usageNotes * * ``` * import { Component } from '@angular/core'; * import { defineRouteMeta } from '@analogjs/router'; * * export const routeMeta = defineRouteMeta({ * title: 'Welcome' * }); * * @Component({ * template: `Home`, * standalone: true, * }) * export default class HomeComponent {} * ``` * * @param route * @returns */ const defineRouteMeta = (route) => { return route; }; /** * Returns the instance of Angular Router * * @returns The router */ const injectRouter = () => { return inject(Router); }; /** * Returns the instance of the Activate Route for the component * * @returns The activated route */ const injectActivatedRoute = () => { return inject(ActivatedRoute); }; function cookieInterceptor(req, next, location = inject(PLATFORM_ID), serverRequest = injectRequest()) { if (isPlatformServer(location) && req.url.includes('/_analog/')) { let headers = new HttpHeaders(); const cookies = serverRequest?.headers.cookie; headers = headers.set('cookie', cookies ?? ''); const cookiedRequest = req.clone({ headers, }); return next(cookiedRequest); } else { return next(req); } } /** * 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 */ function provideFileRouter(...features) { const extraRoutesFeature = features.filter((feat) => feat.ɵkind >= 100); const routerFeatures = features.filter((feat) => feat.ɵkind < 100); return makeEnvironmentProviders([ extraRoutesFeature.map((erf) => erf.ɵproviders), provideRouter(routes, ...routerFeatures), { provide: ENVIRONMENT_INITIALIZER, multi: true, useValue: () => updateMetaTagsOnRouteChange(), }, { provide: _HTTP_ROOT_INTERCEPTOR_FNS, multi: true, useValue: cookieInterceptor, }, { provide: API_PREFIX, useFactory() { return typeof ANALOG_API_PREFIX !== 'undefined' ? ANALOG_API_PREFIX : 'api'; }, }, ]); } /** * Provides extra custom routes in addition to the routes * discovered from the filesystem-based routing. These routes are * inserted before the filesystem-based routes, and take priority in * route matching. */ function withExtraRoutes(routes) { return { ɵkind: 100, ɵproviders: [{ provide: ROUTES, useValue: routes, multi: true }], }; } function injectLoad(options) { const injector = options?.injector ?? inject(Injector); const route = injector.get(ActivatedRoute); return route.data.pipe(map((data) => data['load'])); } /** * Get server load resolver data for the route * * @param route Provides the route to get server load resolver * @returns Returns server load resolver data for the route */ async function getLoadResolver(route) { return route.routeConfig?.resolve?.['load']?.(route); } function sortAndConcatParams(params) { return [...params.keys()] .sort() .map((k) => `${k}=${params.getAll(k)}`) .join('&'); } function makeCacheKey(request, mappedRequestUrl) { // make the params encoded same as a url so it's easy to identify const { params, method, responseType } = request; const encodedParams = sortAndConcatParams(params); let serializedBody = request.serializeBody(); if (serializedBody instanceof URLSearchParams) { serializedBody = sortAndConcatParams(serializedBody); } else if (typeof serializedBody !== 'string') { serializedBody = ''; } const key = [ method, responseType, mappedRequestUrl, serializedBody, encodedParams, ].join('|'); const hash = generateHash(key); return makeStateKey(hash); } function generateHash(str) { let hash = 0; for (let i = 0, len = str.length; i < len; i++) { let chr = str.charCodeAt(i); hash = (hash << 5) - hash + chr; hash |= 0; // Convert to 32bit integer } return `${hash}`; } /** * Interceptor that is server-aware when making HttpClient requests. * Server-side requests use the full URL * Prerendering uses the internal Nitro $fetch function, along with state transfer * Client-side requests use the window.location.origin * * @param req HttpRequest<unknown> * @param next HttpHandlerFn * @returns */ function requestContextInterceptor(req, next) { const apiPrefix = injectAPIPrefix(); const baseUrl = injectBaseURL(); const transferState = inject(TransferState); // during prerendering with Nitro if (typeof global !== 'undefined' && global.$fetch && baseUrl && (req.url.startsWith('/') || req.url.startsWith(baseUrl) || req.url.startsWith(`/${apiPrefix}`))) { const requestUrl = new URL(req.url, baseUrl); const cacheKey = makeCacheKey(req, new URL(requestUrl).pathname); const storeKey = makeStateKey(`analog_${cacheKey}`); const fetchUrl = requestUrl.pathname; const responseType = req.responseType === 'arraybuffer' ? 'arrayBuffer' : req.responseType; return from(global.$fetch .raw(fetchUrl, { method: req.method, body: req.body ? req.body : undefined, params: requestUrl.searchParams, responseType, headers: req.headers.keys().reduce((hdrs, current) => { return { ...hdrs, [current]: req.headers.get(current), }; }, {}), }) .then((res) => { const cacheResponse = { body: res._data, headers: new HttpHeaders(res.headers), status: 200, statusText: 'OK', url: fetchUrl, }; const transferResponse = new HttpResponse(cacheResponse); transferState.set(storeKey, cacheResponse); return transferResponse; })); } // on the client if (!import.meta.env.SSR && (req.url.startsWith('/') || req.url.includes('/_analog/'))) { // /_analog/ requests are full URLs const requestUrl = req.url.includes('/_analog/') ? req.url : `${window.location.origin}${req.url}`; const cacheKey = makeCacheKey(req, new URL(requestUrl).pathname); const storeKey = makeStateKey(`analog_${cacheKey}`); const cacheRestoreResponse = transferState.get(storeKey, null); if (cacheRestoreResponse) { transferState.remove(storeKey); return of(new HttpResponse(cacheRestoreResponse)); } return next(req.clone({ url: requestUrl, })); } // on the server if (baseUrl && (req.url.startsWith('/') || req.url.startsWith(baseUrl))) { const requestUrl = req.url.startsWith(baseUrl) && !req.url.startsWith('/') ? req.url : `${baseUrl}${req.url}`; return next(req.clone({ url: requestUrl, })); } return next(req); } class FormAction { constructor() { this.action = input('', ...(ngDevMode ? [{ debugName: "action" }] : [])); this.onSuccess = output(); this.onError = output(); this.state = output(); this.router = inject(Router); this.route = inject(ActivatedRoute); this.path = this._getPath(); } submitted($event) { $event.preventDefault(); this.state.emit('submitting'); const body = new FormData($event.target); if ($event.target.method.toUpperCase() === 'GET') { this._handleGet(body, this.router.url); } else { this._handlePost(body, this.path, $event); } } _handleGet(body, path) { const params = {}; body.forEach((formVal, formKey) => (params[formKey] = formVal)); this.state.emit('navigate'); const url = path.split('?')[0]; this.router.navigate([url], { queryParams: params, onSameUrlNavigation: 'reload', }); } _handlePost(body, path, $event) { fetch(path, { method: $event.target.method, body, }) .then((res) => { if (res.ok) { if (res.redirected) { const redirectUrl = new URL(res.url).pathname; this.state.emit('redirect'); this.router.navigate([redirectUrl]); } else if (this._isJSON(res.headers.get('Content-type'))) { res.json().then((result) => { this.onSuccess.emit(result); this.state.emit('success'); }); } else { res.text().then((result) => { this.onSuccess.emit(result); this.state.emit('success'); }); } } else { if (res.headers.get('X-Analog-Errors')) { res.json().then((errors) => { this.onError.emit(errors); this.state.emit('error'); }); } else { this.state.emit('error'); } } }) .catch((_) => { this.state.emit('error'); }); } _getPath() { if (this.route) { return injectRouteEndpointURL(this.route.snapshot).pathname; } return `/api/_analog/pages${window.location.pathname}`; } _isJSON(contentType) { const mime = contentType ? contentType.split(';') : []; const essence = mime[0]; return essence === 'application/json'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: FormAction, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.0", type: FormAction, isStandalone: true, selector: "form[action],form[method]", inputs: { action: { classPropertyName: "action", publicName: "action", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSuccess: "onSuccess", onError: "onError", state: "state" }, host: { listeners: { "submit": "submitted($event)" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: FormAction, decorators: [{ type: Directive, args: [{ selector: 'form[action],form[method]', host: { '(submit)': `submitted($event)`, }, standalone: true, }] }] }); const DEBUG_ROUTES = new InjectionToken('@analogjs/router debug routes', { providedIn: 'root', factory() { const debugRoutes = createRoutes({ ...ANALOG_ROUTE_FILES, ...ANALOG_CONTENT_ROUTE_FILES, }, true); return debugRoutes; }, }); function injectDebugRoutes() { return inject(DEBUG_ROUTES); } /** * Provides routes that provide additional * pages for displaying and debugging * routes. */ function withDebugRoutes() { const routes = [ { path: '__analog/routes', loadComponent: () => import('./analogjs-router-debug.page-C7mEWSZu.mjs'), }, ]; return { ɵkind: 101, ɵproviders: [{ provide: ROUTES, useValue: routes, multi: true }], }; } /** * @description * Component that defines the bridge between the client and server-only * components. The component passes the component ID and props to the server * and retrieves the rendered HTML and outputs from the server-only component. * * Status: experimental */ class ServerOnly { constructor() { this.component = input.required(...(ngDevMode ? [{ debugName: "component" }] : [])); this.props = input(...(ngDevMode ? [undefined, { debugName: "props" }] : [])); this.outputs = output(); this.http = inject(HttpClient); this.sanitizer = inject(DomSanitizer); this.content = signal('', ...(ngDevMode ? [{ debugName: "content" }] : [])); this.route = inject(ActivatedRoute, { optional: true }); this.baseURL = injectBaseURL(); this.transferState = inject(TransferState); effect(() => { const routeComponentId = this.route?.snapshot.data['component']; const props = this.props() || {}; const componentId = routeComponentId || this.component(); const headers = new HttpHeaders(new Headers({ 'Content-type': 'application/json', 'X-Analog-Component': componentId, })); const componentUrl = this.getComponentUrl(componentId); const httpRequest = new HttpRequest('POST', componentUrl, props, { headers, }); const cacheKey = makeCacheKey(httpRequest, new URL(componentUrl).pathname); const storeKey = makeStateKey(cacheKey); const componentState = this.transferState.get(storeKey, null); if (componentState) { this.updateContent(componentState); this.transferState.remove(storeKey); } else { this.http .request(httpRequest) .pipe(map((response) => { if (response instanceof HttpResponse) { if (import.meta.env.SSR) { this.transferState.set(storeKey, response.body); } return response.body; } return throwError(() => ({})); }), catchError((error) => { console.log(error); return of({ html: '', outputs: {}, }); })) .subscribe((content) => this.updateContent(content)); } }); } updateContent(content) { this.content.set(this.sanitizer.bypassSecurityTrustHtml(content.html)); this.outputs.emit(content.outputs); } getComponentUrl(componentId) { let baseURL = this.baseURL; if (!baseURL && typeof window !== 'undefined') { baseURL = window.location.origin; } return `${baseURL}/_analog/components/${componentId}`; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ServerOnly, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.0", type: ServerOnly, isStandalone: true, selector: "server-only,ServerOnly,Server", inputs: { component: { classPropertyName: "component", publicName: "component", isSignal: true, isRequired: true, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outputs: "outputs" }, ngImport: i0, template: ` <div [innerHTML]="content()"></div> `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ServerOnly, decorators: [{ type: Component, args: [{ selector: 'server-only,ServerOnly,Server', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div [innerHTML]="content()"></div> `, }] }], ctorParameters: () => [] }); /** * Generated bundle index. Do not edit. */ export { FormAction, ServerOnly, createRoutes, defineRouteMeta, getLoadResolver, injectActivatedRoute, injectDebugRoutes, injectLoad, injectRouteEndpointURL, injectRouter, provideFileRouter, requestContextInterceptor, routes, withDebugRoutes, withExtraRoutes }; //# sourceMappingURL=analogjs-router.mjs.map