UNPKG

@analogjs/router

Version:

Filesystem-based routing for Angular

208 lines (201 loc) 7.08 kB
import { ɵresetCompiledComponents as _resetCompiledComponents, InjectionToken, assertInInjectionContext, inject, TransferState, makeStateKey, reflectComponentType, APP_ID, ɵConsole as _Console, enableProdMode } from '@angular/core'; import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, provideServerRendering, renderApplication } from '@angular/platform-server'; import { REQUEST, RESPONSE, BASE_URL } from '@analogjs/router/tokens'; import { bootstrapApplication } from '@angular/platform-browser'; import { getHeader, createEvent, readBody } from 'h3'; function provideServerContext({ req, res, }) { const baseUrl = getBaseUrl(req); if (import.meta.env.DEV) { _resetCompiledComponents(); } return [ { provide: _SERVER_CONTEXT, useValue: 'ssr-analog' }, { provide: REQUEST, useValue: req }, { provide: RESPONSE, useValue: res }, { provide: BASE_URL, useValue: baseUrl }, ]; } function getBaseUrl(req) { const protocol = getRequestProtocol(req); const { originalUrl, headers } = req; const parsedUrl = new URL('', `${protocol}://${headers.host}${originalUrl.endsWith('/') ? originalUrl.substring(0, originalUrl.length - 1) : originalUrl}`); const baseUrl = parsedUrl.origin; return baseUrl; } function getRequestProtocol(req, opts = {}) { if (opts.xForwardedProto !== false && req.headers['x-forwarded-proto'] === 'https') { return 'https'; } return req.connection?.encrypted ? 'https' : 'http'; } const STATIC_PROPS = new InjectionToken('Static Props'); function provideStaticProps(props) { return { provide: STATIC_PROPS, useFactory() { return props; }, }; } function injectStaticProps() { assertInInjectionContext(injectStaticProps); return inject(STATIC_PROPS); } function injectStaticOutputs() { const transferState = inject(TransferState); const outputsKey = makeStateKey('_analog_output'); return { set(data) { transferState.set(outputsKey, data); }, }; } function serverComponentRequest(serverContext) { const serverComponentId = getHeader(createEvent(serverContext.req, serverContext.res), 'X-Analog-Component'); if (!serverComponentId && serverContext.req.url && serverContext.req.url.startsWith('/_analog/components')) { const componentId = serverContext.req.url.split('/')?.[3]; return componentId; } return serverComponentId; } const components = import.meta.glob([ '/src/server/components/**/*.{ts,analog,ag}', ]); async function renderServerComponent(url, serverContext, config) { const componentReqId = serverComponentRequest(serverContext); const { componentLoader, componentId } = getComponentLoader(componentReqId); if (!componentLoader) { return new Response(`Server Component Not Found ${componentId}`, { status: 404, }); } const component = (await componentLoader())['default']; if (!component) { return new Response(`No default export for ${componentId}`, { status: 422, }); } const mirror = reflectComponentType(component); const selector = mirror?.selector.split(',')?.[0] || 'server-component'; const event = createEvent(serverContext.req, serverContext.res); const body = (await readBody(event)) || {}; const appId = `analog-server-${selector.toLowerCase()}-${new Date().getTime()}`; const bootstrap = (context) => bootstrapApplication(component, { providers: [ provideServerRendering(), provideStaticProps(body), { provide: _SERVER_CONTEXT, useValue: 'analog-server-component' }, { provide: APP_ID, useFactory() { return appId; }, }, ...(config?.providers || []), ], }, context); const html = await renderApplication(bootstrap, { url, document: `<${selector}></${selector}>`, platformProviders: [ { provide: _Console, useFactory() { return { warn: () => { }, log: () => { }, }; }, }, ], }); const outputs = retrieveTransferredState(html, appId); const responseData = { html, outputs, }; return new Response(JSON.stringify(responseData), { headers: { 'X-Analog-Component': 'true', }, }); } function getComponentLoader(componentReqId) { let _componentId = `/src/server/components/${componentReqId.toLowerCase()}`; let componentLoader = undefined; let componentId = _componentId; if (components[`${_componentId}.ts`]) { componentId = `${_componentId}.ts`; componentLoader = components[componentId]; } else if (components[`${componentId}.analog`]) { componentId = `${_componentId}.analog`; componentLoader = components[componentId]; } else if (components[`${componentId}.ag`]) { componentId = `${_componentId}.ag`; componentLoader = components[componentId]; } return { componentLoader, componentId }; } function retrieveTransferredState(html, appId) { const regex = new RegExp(`<script id="${appId}-state" type="application/json">(.*?)<\/script>`); const match = html.match(regex); if (match) { const scriptContent = match[1]; if (scriptContent) { try { const parsedContent = JSON.parse(scriptContent); return parsedContent._analog_output || {}; } catch (e) { console.warn('Exception while parsing static outputs for ' + appId, e); } } return {}; } else { return {}; } } if (import.meta.env.PROD) { enableProdMode(); } /** * Returns a function that accepts the navigation URL, * the root HTML, and server context. * * @param rootComponent * @param config * @param platformProviders * @returns Promise<string | Reponse> */ function render(rootComponent, config, platformProviders = []) { function bootstrap(context) { return bootstrapApplication(rootComponent, config, context); } return async function render(url, document, serverContext) { if (serverComponentRequest(serverContext)) { return await renderServerComponent(url, serverContext); } const html = await renderApplication(bootstrap, { document, url, platformProviders: [ provideServerContext(serverContext), platformProviders, ], }); return html; }; } /** * Generated bundle index. Do not edit. */ export { injectStaticOutputs, injectStaticProps, provideServerContext, render, renderServerComponent, serverComponentRequest }; //# sourceMappingURL=analogjs-router-server.mjs.map