Slow TTFB with Angular Universal and Express - node.js

I'm posting this as a self-answered question because I didn't find anything that helped on the web when I was debugging this issue.
For some reason, the SSR version of my Angular 7 application consistently has a TTFB of around 5 seconds. I'm using the standard setup for Angular Universal using Express with a few modifications like so:
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModuleFactory } from '#angular/platform-server';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
import { provideModuleMap } from '#nguniversal/module-map-ngfactory-loader';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const compression = require('compression');
const app = express();
app.use(compression());
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');
const domino = require('domino');
const template = readFileSync(join(DIST_FOLDER , 'browser', 'index.html')).toString();
const win = domino.createWindow(template);
global['window'] = win;
global['Node'] = win.Node;
global['navigator'] = win.navigator;
global['Event'] = win.Event;
global['KeyboardEvent'] = win.Event;
global['MouseEvent'] = win.Event;
global['Event']['prototype'] = win.Event.prototype;
global['document'] = win.document;
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
app.engine('html', (_, options, callback) => {
renderModuleFactory(AppServerModuleNgFactory, {
// Our index.html
document: template,
url: options.req.url,
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
}).then(html => {
callback(null, html);
});
});
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
I've tried caching the pages on the server and removing the non-standard Domino stuff to make it just like the tutorial, but nothing changes the TTFB. That leads me to believe there is something in the application code that could be slowing it down. Any ideas?

When thinking about it more, I remembered that I had added a 5 second delay on pre-loading modules so that users who bounce from the application don't end up having to download all of the lazy loaded bundles. The code was like so:
import { PreloadingStrategy } from "#angular/router";
import { Route } from "#angular/router";
import { RouterModule } from "#angular/router";
import { Routes } from "#angular/router";
import { NgModule } from "#angular/core";
import { Observable, timer } from "rxjs";
import { of } from 'rxjs/internal/observable/of';
import { flatMap } from "rxjs/internal/operators";
export class PreloadPublicModules implements PreloadingStrategy {
preload(route : Route, load : Function) : Observable<any> {
const loadRoute = (delay) => delay ? timer(5000).pipe(flatMap(_ => load())) : load();
return route.data && route.data.preload ? loadRoute(5000) : of(null);
}
}
const routes : Routes = [
{ path: '', loadChildren: "app/public/home/home.module#HomeModule", data: { preload: true } },
{ path: 'about', loadChildren: "app/public/about/about.module#AboutModule", data: { preload: true } },
{ path: 'contact', loadChildren: "app/public/contact/contact.module#ContactModule", data: { preload: true } },
{ path: 'dashboard', loadChildren: "app/internal/dashboard/dashboard.module#DashboardModule", data: { preload: false } },
];
#NgModule({
imports : [ RouterModule.forRoot(routes, { preloadingStrategy: PreloadPublicModules }) ],
exports : [ RouterModule ],
providers : [ PreloadPublicModules ]
})
export class AppRoutingModule { }
Removing that delay fixed the issue for the SSR portion; it no longer has the 5s TTFB. The PreloadPublicModules function is now:
export class PreloadPublicModules implements PreloadingStrategy {
preload(route : Route, load : Function) : Observable<any> {
return route.data && route.data.preload ? load() : of(null);
}
}

Related

StreamMessageReader is not a constructor in Electron + Monaco app

I'm trying to build an application using Electron with Monaco editor. I used monaco language client to integrate the editor with clangd. But somehow it didn't work... After manually connecting to the LSP server through web socket, I get the following error in the terminal:
TypeError: node.exports.StreamMessageReader is not a constructor
at createStreamConnection (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3100:18)
at createProcessStreamConnection (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3094:12)
at createServerProcess (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3090:10)
at launch (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3114:28)
at /Volumes/Data/Develop/cpcode/dist/electron/main.js:3160:13
at WebSocketServer.completeUpgrade (/Volumes/Data/Develop/cpcode/node_modules/ws/lib/websocket-server.js:431:5)
at WebSocketServer.handleUpgrade (/Volumes/Data/Develop/cpcode/node_modules/ws/lib/websocket-server.js:339:10)
at Server.<anonymous> (/Volumes/Data/Develop/cpcode/dist/electron/main.js:3147:13)
at Server.emit (node:events:526:28)
at onParserExecuteCommon (node:_http_server:727:14)
I am starting the server in Electron main process. Here's the code for it:
process.env.DIST = join(__dirname, '..')
process.env.PUBLIC = app.isPackaged ? process.env.DIST : join(process.env.DIST, '../public')
import { join } from 'path'
import { app, BrowserWindow } from 'electron'
import ws from "ws";
import http from "http";
import url from "url";
import net from "net";
import express from "express";
import * as rpc from "vscode-ws-jsonrpc";
import * as server from "vscode-ws-jsonrpc/server";
import * as lsp from "vscode-languageserver";
import { Message } from "vscode-languageserver";
export function launch(socket: rpc.IWebSocket) {
const reader = new rpc.WebSocketMessageReader(socket);
const writer = new rpc.WebSocketMessageWriter(socket);
// start the language server as an external process
const socketConnection = server.createConnection(reader, writer, () =>
socket.dispose()
);
const serverConnection = server.createServerProcess("CPP", "clangd");
if (serverConnection) {
server.forward(socketConnection, serverConnection, (message) => {
if (Message.isRequest(message)) {
console.log("server request!!!");
if (message.method === lsp.InitializeRequest.type.method) {
const initializeParams = message.params as lsp.InitializeParams;
initializeParams.processId = process.pid;
}
}
return message;
});
}
}
process.on("uncaughtException", function (err: any) {
console.error("Uncaught Exception: ", err.toString());
if (err.stack) {
console.error(err.stack);
}
});
export const startLSP = () => {
// create the express application
const app = express();
// server the static content, i.e. index.html
app.use(express.static(__dirname));
// start the server
const server = app.listen(3000);
// create the web socket
const wss = new ws.Server({
noServer: true,
perMessageDeflate: false
});
server.on(
"upgrade",
(request: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
// eslint-disable-next-line n/no-deprecated-api
const pathname = request.url
? url.parse(request.url).pathname
: undefined;
if (pathname === "/lsp") {
wss.handleUpgrade(request, socket, head, (webSocket) => {
const socket: rpc.IWebSocket = {
send: (content) =>
webSocket.send(content, (error) => {
if (error) {
throw error;
}
}),
onMessage: (cb) => webSocket.on("message", cb),
onError: (cb) => webSocket.on("error", cb),
onClose: (cb) => webSocket.on("close", cb),
dispose: () => webSocket.close()
};
// launch the server when the web socket is opened
if (webSocket.readyState === webSocket.OPEN) {
launch(socket);
} else {
webSocket.on("open", () => launch(socket));
}
});
}
}
);
};
let win: BrowserWindow | null
// Here, you can also use other preload
const preload = join(__dirname, './preload.js')
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite#2.x
const serverURL = process.env['VITE_DEV_SERVER_URL']
function createWindow() {
win = new BrowserWindow({
icon: join(process.env.PUBLIC, 'logo.svg'),
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
preload,
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'hidden',
})
startLSP();
// Test active push message to Renderer-process.
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', (new Date).toLocaleString())
})
if (app.isPackaged) {
win.loadFile(join(process.env.DIST, 'index.html'))
} else {
win.loadURL(serverURL)
}
}
app.on('window-all-closed', () => {
win = null
})
app.whenReady().then(createWindow)
I am using Vite as a bundler and I'm not sure if it has anything to do with this. Anyway, here's my vite.config.ts:
import fs from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import electron from 'vite-plugin-electron'
import renderer from 'vite-plugin-electron-renderer';
import { nodePolyfills } from 'vite-plugin-node-polyfills'
fs.rmSync('dist', { recursive: true, force: true }) // v14.14.0
export default defineConfig({
plugins: [
electron({
main: {
entry: 'electron/main.ts',
},
preload: {
input: {
// Must be use absolute path, this is the restrict of Rollup
preload: path.join(__dirname, 'electron/preload.ts'),
},
},
// Enables use of Node.js API in the Renderer-process
// https://github.com/electron-vite/vite-plugin-electron/tree/main/packages/electron-renderer#electron-renderervite-serve
renderer: {},
}),
nodePolyfills({
// Whether to polyfill `node:` protocol imports.
protocolImports: true,
}),
],
})
After Googling, I tried to install vscode-languageserver and vscode-languageserver-protocol, but no luck. The problem still continues.
Does anyone has a solution to this? Thanks in advance!

Nestjs graphql upload error: POST body missing. Did you forget use body-parser middleware?

I'm using NestJS 7 with graphql on node v14x
This is my graphql module configuration
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '#nestjs/common';
import { graphqlUploadExpress } from "graphql-upload"
import { GraphQLModule } from '#nestjs/graphql';
import { ConfigService } from '#nestjs/config';
import { ApolloServerDataLoaderPlugin } from '#webundsoehne/nestjs-graphql-typeorm-dataloader';
import { GraphqlConfig } from './#core/config/config.interface';
import { getConnection } from 'typeorm';
#Module({
imports: [
GraphQLModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
const graphqlConfig = configService.get<GraphqlConfig>('graphql');
return {
buildSchemaOptions: {
numberScalarMode: 'integer',
plugins: [new ApolloServerDataLoaderPlugin({ typeormGetConnection: getConnection })]
},
sortSchema: graphqlConfig.sortSchema,
autoSchemaFile:
graphqlConfig.schemaDestination || './src/schema.graphql',
debug: graphqlConfig.debug,
path: graphqlConfig.endPoint,
uploads: false,
playground: graphqlConfig.playgroundEnabled,
context: ({ req, connection }) => {
if (connection) {
return { req: { headers: connection.context } }
}
return { req }
},
installSubscriptionHandlers: true,
dateScalarMode: "isoDate",
subscriptions: {
keepAlive: 5000
}
};
},
inject: [ConfigService],
}),
],
})
export class GQLModule implements NestModule {
constructor(private readonly configService: ConfigService) { }
configure(consumer: MiddlewareConsumer) {
const graphqlConfig = this.configService.get<GraphqlConfig>('graphql');
consumer.apply(graphqlUploadExpress()).forRoutes(graphqlConfig.endPoint)
}
}
After getting stuck on file upload not working on node v14.x, I find this issue comment. And I'm importing everything from graphql-upload on my resolver, But still getting error message like POST body missing. Did you forget use body-parser middleware?
Can anybody help me on this?
I have nearly identical configuration, and it works correctly for me. Since I use both REST and GraphQL, I have limited the route to 'graphql'
import { MiddlewareConsumer, Module, NestModule } from '#nestjs/common';
import { ServeStaticModule } from '#nestjs/serve-static';
import { MongooseModule } from '#nestjs/mongoose';
import { GraphQLModule } from '#nestjs/graphql';
import { AuthorModule } from '../author/author.module';
import { ApiService } from './api.service';
import { join } from 'path';
const {
graphqlUploadExpress,
} = require('graphql-upload');
const isLocalEnvironment = () => process.env.ENV === 'local';
console.log('isLocal', isLocalEnvironment(), process.env.ENV);
const modules = [
AuthorModule,
GraphQLModule.forRoot({
playground: isLocalEnvironment(),
fieldResolverEnhancers: ['guards'],
debug: isLocalEnvironment(),
autoSchemaFile: isLocalEnvironment() ? 'schema.gql' : true,
introspection: isLocalEnvironment(),
useGlobalPrefix: true,
context: ({ req }) => ({ req }),
}),
MongooseModule.forRoot(process.env.COSMOSDB_CONNECTION_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
retryWrites: false,
autoIndex: true,
}),
];
#Module({
imports: modules,
providers: [ApiService],
})
export class APIModule implements NestModule{
configure(consumer: MiddlewareConsumer) {
consumer.apply(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 })).forRoutes('graphql');
}
}
In my main.ts file, I bootstrap the application.
if (process.env.NODE_ENV !== 'production') require('dotenv').config();
import { NestFactory } from '#nestjs/core';
import { APIModule } from './api/api.module';
import { INestApplication } from '#nestjs/common';
import { json, urlencoded } from 'body-parser';
import 'isomorphic-fetch';
const prefix = "api/";
async function bootstrap() {
const port = process.env.PORT;
const app: INestApplication = await NestFactory.create(APIModule);
app.enableCors();
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: true }));
// Because I use the global prefix of api/, I need to hit my service at /api/graphql
app.setGlobalPrefix(prefix);
console.log('Starting on port ' + port);
await app.listen(port);
return app;
}
bootstrap();
Since your configuration looks good to me, I'm guessing something went wrong in your bootstrap function, or that you aren't hitting the /graphql route.
I am using the latest version of NestJS which no longer includes a graphql upload implementation, so unlike you, I do not need to turn uploads off.

R3InjectorError(AppServerModule) No provider for InjectionToken compilerOptions

I am getting the following error:
render error Error [NullInjectorError]: R3InjectorError(AppServerModule)[RouterModule -> Router -> NgModuleFactoryLoader -> Compiler -> InjectionToken compilerOptions -> InjectionToken compilerOptions -> InjectionToken compilerOptions]:
NullInjectorError: No provider for InjectionToken compilerOptions!
I followed the instructions provided by https://angular.io/guide/universal but it seems that few adjustments I needed to add like using domino to simulate document object to work. One of the issues that's making it very hard for me to find out where is the problem is that the errors are pointed in the main.js
render error Error [NullInjectorError]: R3InjectorError(AppServerModule)[RouterModule -> Router -> NgModuleFactoryLoader -> Compiler -> InjectionToken compilerOptions -> InjectionToken compilerOptions -> InjectionToken compilerOptions]:
NullInjectorError: No provider for InjectionToken compilerOptions!
at ɵɵinject (<project-dir>\dist\apps\ui\server\main.js:219196:57)
at injectArgs (<project-dir>\dist\apps\ui\server\main.js:219291:23)
at Object.factory (<project-dir>\dist\apps\ui\server\main.js:229671:52)
at R3Injector.hydrate (<project-dir>\dist\apps\ui\server\main.js:229582:35)
at R3Injector.get (<project-dir>\dist\apps\ui\server\main.js:229404:33) {
ngTempTokenPath: null,
ngTokenPath: [
'RouterModule',
'Router',
'NgModuleFactoryLoader',
'Compiler',
'InjectionToken compilerOptions',
'InjectionToken compilerOptions',
'InjectionToken compilerOptions'
]
} undefined
I am not able to get ng run dev:ssr to show typescript instead.
Here is the entry server.ts:
import { ngExpressEngine } from '#nguniversal/express-engine';
import 'zone.js/dist/zone-node';
// import * as express from 'express';
const express = require('express');
import { APP_BASE_HREF } from '#angular/common';
import { Express, Request, Response } from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import { uiTransports } from 'src/server/transports/transports.winston';
import * as winston from 'winston';
const domino = require('domino');
const fs = require('fs');
import 'localstorage-polyfill';
const winstonLogger = winston.createLogger({
transports: uiTransports,
});
winstonLogger.info('Starting up UI Server');
// Simulate browser
const distFolder = join(process.cwd(), `dist/apps/ui/browser`);
winstonLogger.info(`Browser dist folder is located at ${distFolder}`);
const templateA = fs.readFileSync(join(distFolder, 'index.html')).toString();
const win = domino.createWindow(templateA);
win.Object = Object;
win.Math = Math;
// (global as any).console = winstonLogger;
(global as any).window = win;
(global as any).document = win.document;
(global as any).branch = null;
(global as any).object = win.object;
(global as any).HTMLElement = win.HTMLElement;
(global as any).navigator = win.navigator;
(global as any).localStorage = localStorage;
(global as any).sessionStorage = localStorage;
(global as any).getComputedStyle = () => {
return {
getPropertyValue() {
return '';
},
};
};
// Browser simulation end
import { AppServerModule } from './src/main.server';
// The Express app is exported so that it can be used by serverless Functions.
export function app(): Express {
const server: Express = express();
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
winstonLogger.info(`Index HTML located at ${indexHtml}`);
// Our Universal express-engine (found # https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModule,
}),
);
server.set('view engine', 'html');
server.set('views', distFolder);
winstonLogger.info('Engine setup completed');
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get(
'*.*',
express.static(distFolder, {
maxAge: '1y',
}),
);
// All regular routes use the Universal engine
server.get('*', (req: Request, res: Response) => {
try {
winstonLogger.info(`Rendering for request ${req.url} started`);
res.render(
indexHtml,
// templateA,
{
req,
res,
// url: req.url,
// cookies: req.cookies,
// bootstrap: AppServerModule,
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
},
(err: Error, html: string) => {
console.error('render error', err, html);
res.status(html ? 200 : 500).send(html || err.message);
},
);
winstonLogger.info(`Rendering for request ${req.url} completed`);
} catch (ex) {
winstonLogger.error('error from server', ex);
}
});
return server;
}
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app();
winstonLogger.info(`Server is about to start to listen to port ${port}`);
server.listen(port, () => {
winstonLogger.info(`Node Express server listening on http://localhost:${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = (mainModule && mainModule.filename) || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';
Here is app.server.module.ts
import { NgModule } from '#angular/core';
import { ServerModule } from '#angular/platform-server';
import { IonicServerModule } from '#ionic/angular-server'; // i added this thinking it might help
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
#NgModule({
imports: [AppModule, ServerModule, IonicServerModule],
bootstrap: [AppComponent],
})
export class AppServerModule {}
Any advises would be much appreciated!
Edit:
It turns out that the error is coming from deps: [[Optional(), COMPILER_OPTIONS]], below:
#NgModule({
declarations: [DynamicComponentDirective],
exports: [DynamicComponentDirective],
})
export class DynamicComponentModule {
static forRoot(metadata: NgModule): ModuleWithProviders<DynamicComponentModule> {
console.log('compiler options');
return {
ngModule: DynamicComponentModule,
providers: [
{
provide: Compiler,
useFactory: createJitCompiler,
/**
* This leads to the following error:
* R3InjectorError(AppServerModule)[RouterModule -> Router -> NgModuleFactoryLoader -> Compiler -> InjectionToken compilerOptions -> InjectionToken compilerOptions -> InjectionToken compilerOptions]: NullInjectorError: No provider for InjectionToken compilerOptions!
*/
// deps: [[Optional(), COMPILER_OPTIONS]],
},
{
provide: DynamicComponentOptions,
useValue: {
ngModuleMetadata: metadata,
},
},
],
};
}
}
I am assuming that I need to inject it in the AppServerModule
#NgModule({
imports: [
...
],
bootstrap: [...],
providers: [/* I think i need to add a provider for it here */],
})
export class AppServerModule {}
Not sure how, can anyone help please?
It turns out it was an issue with the angular.json configuration for the SSR, following the same config as https://github.com/Angular-RU/universal-starter/blob/master/angular.json resolved the issue.

Errors on running express Server with Node.js with Universal Angular 5

I have tweo problems.
First, I can't pass a queryParam from server.ts to AppCompoment.ts.
Second, the service translateService throws ERROR [Error] without message when is executed on the server, but works fine when is executed on the browser.
Here the code parts of the projec:
server.ts
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { renderModuleFactory } from '#angular/platform-server';
import { enableProdMode } from '#angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
(global as any).WebSocket = require('ws');
(global as any).XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
//app.urlencoded({extended: false});
const PORT = process.env.PORT || 4000;
const HTTPS_PORT = process.env.HTTPS_PORT || 4443;
const KEY_CERTIFICATE = process.env.KEY_CERTIFICATE;
const CRT_CERTIFICATE = process.env.CRT_CERTIFICATE;
const PASSWORD_CERTIFICATE = process.env.PASSWORD_CERTIFICATE;
const DIST_FOLDER = join(process.cwd(), 'dist');
// Our index.html we'll use as our template
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
// Express Engine
import { ngExpressEngine } from '#nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '#nguniversal/module-map-ngfactory-loader';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Our page routes
export const routes: string[] = [
'main',
'dashboard',
'dashboard/contact',
'dashboard/blog'
];
// All regular routes use the Universal engine
app.get('/', (req, res) => {
console.time(`GET: ${req.originalUrl}`);
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req, res } );
console.timeEnd(`GET: ${req.originalUrl}`);
});
routes.forEach(route => {
app.get(`/${route}`, (req, res) => {
//res.json({'lang': req.query.lang});
console.log(req.query.lang);
console.time(`GET: ${req.originalUrl}`);
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req, res } );
console.timeEnd(`GET: ${req.originalUrl}`);
});
app.get(`/${route}/*`, (req, res) => {
//res.json({'lang': req.query.lang});
console.log(req.query.lang);
console.time(`GET: ${req.originalUrl}`);
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req, res } );
console.timeEnd(`GET: ${req.originalUrl}`);
});
});
// Server static files from /browser
app.get('/web', express.static(join(DIST_FOLDER, 'browser'), { 'index': false }));
app.get('/**', express.static(join(DIST_FOLDER, 'browser')));
// All other routes must be resolved if exist
/*
app.get('*', function(req, res) {
res.render(join(req.url), { req });
});
*/
var http = require('http');
var httpServer = http.createServer(app);
// Start up the Node server at PORT
httpServer.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
if(KEY_CERTIFICATE && CRT_CERTIFICATE && PASSWORD_CERTIFICATE) {
var fs = require('fs');
var https = require('https');
var privateKey = fs.readFileSync(KEY_CERTIFICATE, 'utf8');
var certificate = fs.readFileSync(CRT_CERTIFICATE, 'utf8');
var credentials = {
key: privateKey,
cert: certificate,
passphrase: PASSWORD_CERTIFICATE
};
var httpsServer = https.createServer(credentials, app);
// Start up the Node server at HTTP_PORT
httpsServer.listen(HTTPS_PORT, () => {
console.log(`Node server listening on http://localhost:${HTTPS_PORT}`);
});
}
AppServerModule.ts
import { NgModule } from '#angular/core';
import { ServerModule, ServerTransferStateModule } from '#angular/platform-server';
import { ModuleMapLoaderModule } from '#nguniversal/module-map-ngfactory-loader';
import { AppModule } from './app.module';
import { TemplateComponent } from '../template/template.component';
#NgModule({
imports: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from #angular/platform-server.
AppModule,
ServerModule,
ModuleMapLoaderModule, // <-- *Important* to have lazy-loaded routes work
ServerTransferStateModule,
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [TemplateComponent],
})
export class AppServerModule {
constructor() {
console.log('AppServerModule');
}
}
AppModule:
import { NgModule } from '#angular/core';
import { BrowserModule, BrowserTransferStateModule } from '#angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { enableProdMode } from '#angular/core';
import { HttpClientModule, HttpClient } from '#angular/common/http';
import { TranslateHttpLoader } from '#ngx-translate/http-loader';
import { TranslateModule, TranslateLoader } from '#ngx-translate/core';
// Routing
import { Router } from '#angular/router';
import { RouterModule, Routes } from '#angular/router';
// Firebase
import { AngularFireModule } from 'angularfire2';
import { environment } from '../../environments/environment';
import { AngularFireDatabase } from 'angularfire2/database';
import { AngularFireAuthModule } from 'angularfire2/auth';
import { AuthService } from '../firebase-auth/auth.service';
import { FirebaseAuthComponent } from '../firebase-auth/firebase-auth.component';
// My Components
import { TemplateComponent } from '../template/template.component';
import { MainComponent } from '../main/main.component';
import { BlogComponent } from '../blog/blog.component';
import { ProjectsComponent } from '../projects/projects.component';
import { ProjectComponent } from '../project/project.component';
import { FormComponent } from '../form/form.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
import { DashboardBlogComponent } from '../dashboard/dashboard-blog.component';
import { DashboardContactComponent } from '../dashboard/dashboard-contact.component';
import { ExperienceComponent } from '../experience/experience.component';
import { ProjectService } from '../projects/project.service';
import { ExperienceService } from '../experience/experience.service';
import { PipesModule } from '../pipes/pipes.module';
import { LanguageService } from '../template/language.service';
const appRoutes: Routes = [
{
path: 'main',
component: MainComponent
},
{
path: 'blog',
component: BlogComponent
},
{
path: 'dashboard',
component: DashboardComponent,
children: [
{
path: '',
redirectTo: 'blog',
pathMatch: 'full'
},
{
path: 'blog',
component: DashboardBlogComponent
},
{
path: 'contact',
component: DashboardContactComponent
}
]
},
{
path: '',
redirectTo: '/main',
pathMatch: 'prefix'
},
{
path: '**',
redirectTo: '/main',
pathMatch: 'prefix'
}
];
#NgModule({
declarations: [
FirebaseAuthComponent,
MainComponent,
BlogComponent,
TemplateComponent,
ProjectsComponent,
ProjectComponent,
FormComponent,
DashboardComponent,
DashboardBlogComponent,
DashboardContactComponent,
ExperienceComponent,
],
imports: [
BrowserModule.withServerTransition({appId: 'davidmartinezros.com'}),
BrowserTransferStateModule,
FormsModule,
ReactiveFormsModule,
HttpModule,
HttpClientModule,
RouterModule.forRoot(appRoutes),
AngularFireModule.initializeApp(environment.firebase),
AngularFireAuthModule,
PipesModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient]
}
})
],
providers: [AuthService, AngularFireDatabase, BlogComponent, DashboardContactComponent, ProjectService, ExperienceService, LanguageService],
bootstrap: [TemplateComponent] // main (first) component
})
export class AppModule {
constructor() {
console.log('AppModule');
}
}
/*
if (!/localhost/.test(document.location.host)) {
enableProdMode();
}
*/
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
TemplateComponent.ts:
import { Component, ViewChild, ElementRef, Inject } from '#angular/core';
import { ActivatedRoute, RouterModule } from '#angular/router';
import { AuthService } from '../firebase-auth/auth.service';
import { TranslateService } from '#ngx-translate/core';
import { ProjectsComponent } from '../projects/projects.component';
import { ExperienceComponent } from '../experience/experience.component';
import { LanguageService } from './language.service';
import { Pipe } from '#angular/core';
import { Language } from './language';
import { Title, Meta } from '#angular/platform-browser';
import { PLATFORM_ID } from '#angular/core';
import { isPlatformBrowser, isPlatformServer } from '#angular/common';
#Component({
selector: 'app-template',
templateUrl: '../template/template.component.html'
})
export class TemplateComponent {
objectKeys = Object.keys;
languages: Language[];
language;
loaded: boolean = false;
playing: boolean = false;
sound: any;
constructor(#Inject(PLATFORM_ID) private platformId: Object,
public authService: AuthService,
private translate: TranslateService,
private route: ActivatedRoute,
private languageService: LanguageService,
private titleService: Title,
private metaService: Meta) {
this.getLanguanges();
}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
this.loadLanguage();
this.loadMusic();
}
if (isPlatformServer(this.platformId)) {
// Server only code.
this.loadServerLanguage();
}
}
loadMusic() {
this.sound = new Audio();
this.sound.autoplay = false;
this.sound.preload = 'auto';
this.sound.autobuffer = true;
let parent = this;
this.sound.addEventListener('loadeddata', function() {
parent.loaded = true;
}, false);
this.sound.addEventListener('play', function() {
parent.playing = true;
}, false);
this.sound.addEventListener('pause', function() {
parent.playing = false;
}, false);
this.sound.src = './assets/audio/Rhodesia_MkII.mp3';
this.sound.load();
}
loadLanguage() {
var userLang = "";
//console.log(this.route);
console.log(this.route.queryParams);
this.route.queryParams.subscribe(params => {
if(!params['lang'] || params['lang'] == "") {
userLang = this.language;
} else {
userLang = params['lang'];
}
console.log("queryParams:" + userLang);
if(!userLang || userLang == "") {
userLang = navigator.language;
if(userLang.startsWith("zh")) {
userLang = "zh";
}
}
if(userLang) {
userLang = userLang.toLowerCase();
}
if(userLang && userLang.length > 2) {
userLang = userLang.substring(0,2);
}
if(userLang == "es" || userLang == "en" || userLang == "zh") {
this.changeLanguage(userLang);
} else {
this.changeLanguage("en");
}
console.log('complete loadLanguage');
});
}
loadServerLanguage() {
var userLang = "";
//console.log(this.route);
console.log(this.route.queryParams);
this.route.queryParams.subscribe(params => {
if(!params['lang'] || params['lang'] == "") {
userLang = this.language;
} else {
userLang = params['lang'];
}
console.log("queryParams:" + userLang);
if(userLang) {
userLang = userLang.toLowerCase();
}
if(userLang && userLang.length > 2) {
userLang = userLang.substring(0,2);
}
if(userLang == "es" || userLang == "en" || userLang == "zh") {
this.changeLanguage(userLang);
} else {
this.changeLanguage("en");
}
console.log('complete loadLanguage');
});
}
isLoadedTrack() {
return this.loaded;
}
isPlayingTrack() {
return this.playing;
}
playTrack() {
if(this.sound) {
this.sound.play();
}
}
pauseTrack() {
if(this.sound) {
this.sound.pause();
}
}
public changeServerLanguage(language) {
console.log(language);
console.log("Ara anem a cridar a this.translate.setDefaultLang(language); que al servidor dona error.");
// this language will be used as a fallback when a translation isn't found in the current language
this.translate.setDefaultLang(language);
// the lang to use, if the lang isn't available, it will use the current loader to get them
this.translate.use(language);
this.language = language;
// Sets the <title></title>
this.translate.get("TitleIndex")
.toPromise()
.then(title => this.titleService.setTitle(title))
.catch(this.handleError);
// Sets the <meta> tag author
this.translate.get("TagAuthorIndex")
.toPromise()
.then(author => this.metaService.updateTag({ name: 'author', content: author }))
.catch(this.handleError);
// Sets the <meta> tag keywords
this.translate.get("TagKeywordsIndex")
.toPromise()
.then(keywords => this.metaService.updateTag({ name: 'keywords', content: keywords }))
.catch(this.handleError);
// Sets the <meta> tag description
this.translate.get("TagDescriptionIndex")
.toPromise()
.then(description => this.metaService.updateTag({ name: 'description', content: description }))
.catch(this.handleError);
console.log('changeServerLanguage');
}
public changeLanguage(language) {
console.log(language);
// this language will be used as a fallback when a translation isn't found in the current language
this.translate.setDefaultLang(language);
// the lang to use, if the lang isn't available, it will use the current loader to get them
this.translate.use(language);
this.language = language;
ProjectsComponent.updateStuff.next(false);
ExperienceComponent.updateStuff.next(false);
this.getLanguanges();
// Sets the <title></title>
this.translate.get("TitleIndex")
.toPromise()
.then(title => this.titleService.setTitle(title))
.catch(this.handleError);
// Sets the <meta> tag author
this.translate.get("TagAuthorIndex")
.toPromise()
.then(author => this.metaService.updateTag({ name: 'author', content: author }))
.catch(this.handleError);
// Sets the <meta> tag keywords
this.translate.get("TagKeywordsIndex")
.toPromise()
.then(keywords => this.metaService.updateTag({ name: 'keywords', content: keywords }))
.catch(this.handleError);
// Sets the <meta> tag description
this.translate.get("TagDescriptionIndex")
.toPromise()
.then(description => this.metaService.updateTag({ name: 'description', content: description }))
.catch(this.handleError);
}
getLanguanges(): void {
this.languageService.getLanguages()
.then(languages =>
{ this.languages = languages }
);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
}

Getting found-relay to work with universal-webpack

I'm encountering this error on npm run start:
TypeError: Cannot read property 'pathname' of undefined
Which is getting thrown by:
ReactDOMServer.renderToString(element)
Edit: Got some better logging and found that createResolver(fetcher) is returning an object with:
lastQueries: [ undefined, undefined ],
/src/server.js
import path from 'path';
import express from 'express';
import bodyParser from 'body-parser';
import httpProxy from 'http-proxy';
import { getFarceResult } from 'found/lib/server';
import ReactDOMServer from 'react-dom/server';
import serialize from 'serialize-javascript';
import { ServerFetcher } from './fetcher';
import { createResolver, historyMiddlewares, render, routeConfig } from './router';
const {PRIVATE_IP, API_IP, PORT, API_PORT} = process.env;
const publicPath = path.join(__dirname, '/..', 'public');
export default parameters => {
const app = express();
const proxy = httpProxy.createProxyServer({ ignorePath: true });
const proxyOptions = {
target: `http://${API_IP}:${API_PORT}/graphql-api`,
ignorePath: true,
};
function getFromProxy (req, res) {
req.removeAllListeners('data');
req.removeAllListeners('end');
process.nextTick(_ => {
if (req.body) {
req.emit('data', JSON.stringify(req.body));
}
req.emit('end');
});
proxy.web(req, res, proxyOptions);
}
app.use(express.static(publicPath));
app.use(bodyParser.json({ limit: '1mb' }));
app.use('/graphql-api', getFromProxy);
app.use(async (req, res) => {
const fetcher = new ServerFetcher(`http://${PRIVATE_IP}:${PORT}/graphql-api`);
const { redirect, status, element } = await getFarceResult({
url: req.url,
historyMiddlewares,
routeConfig,
resolver: createResolver(fetcher),
render,
});
if (redirect) {
res.redirect(302, redirect.url);
return;
}
res.status(status).send(`
<!DOCTYPE html>
<html lang="en">
...
<body>
<div id="root">${ReactDOMServer.renderToString(element)}</div>
<script>
window.__RELAY_PAYLOADS__ = ${serialize(fetcher, { isJSON: true })};
</script>
<script src="/bundle.js"></script>
</body>
</html>
`);
});
app.listen(PORT, PRIVATE_IP, err => {
if (err) {
console.log(`[Error]: ${err}`);
}
console.info(`[express server]: listening on ${PRIVATE_IP}:${PORT}`);
});
};
Other files:
/package.json
"scripts": {
"schema": "gulp load-schema",
"relay": "relay-compiler --src ./src --schema ./data/schema.graphql",
"start": "npm-run-all schema relay prepare-server-build start-development-workflow",
"start-development-workflow": "npm-run-all --parallel development-webpack-build-for-client development-webpack-build-for-server development-start-server",
"prepare-server-build": "universal-webpack --settings ./webpack.isomorphic.settings.json prepare",
"development-webpack-build-for-client": "webpack-dev-server --hot --inline --config \"./webpack.isomorphic.client.babel.js\" --port 8080 --colors",
"development-webpack-build-for-server": "webpack --watch --config \"./webpack.isomorphic.server.babel.js\" --colors",
"development-start-server": "nodemon \"./start-server.babel\" --watch \"./build/dist/server\"",
...
},
/start-server.babel
require('babel-register')({ ignore: /\/(build|node_modules)\// });
require('babel-polyfill');
require('./src/start-server.js');
/src/start-server
import 'source-map-support/register';
import startServer from 'universal-webpack/server';
import settings from '../webpack.isomorphic.settings.json';
import configuration from '../webpack.config';
startServer(configuration, settings);
/webpack.config
import webpack from 'webpack';
import path from 'path';
import env from 'gulp-env';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
// process.traceDeprecation = true;
if (!process.env.NODE_ENV) {
env({file: './.env', type: 'ini'});
}
const {
NODE_ENV,
PRIVATE_IP,
API_IP,
PORT,
API_PORT,
GOOGLE_ANALYTICS_KEY,
} = process.env;
const PATHS = {
root: path.join(__dirname),
src: path.join(__dirname, 'src'),
public: path.join(__dirname, 'build', 'public'),
shared: path.join(__dirname, 'src', 'shared'),
fonts: path.join(__dirname, 'src', 'shared', 'fonts'),
robots: path.join(__dirname, 'src', 'robots.txt'),
};
let devtool;
const plugins = [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(NODE_ENV),
PRIVATE_IP: JSON.stringify(PRIVATE_IP),
API_IP: JSON.stringify(API_IP),
PORT: JSON.stringify(PORT),
API_PORT: JSON.stringify(API_PORT),
GOOGLE_ANALYTICS_KEY: JSON.stringify(GOOGLE_ANALYTICS_KEY),
},
}),
new ExtractTextPlugin('styles.css'),
];
if (NODE_ENV === 'production') {
devtool = 'source-map';
plugins.push(
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
},
}),
new webpack.NoErrorsPlugin(),
new CopyWebpackPlugin([
{ from: PATHS.robots, to: PATHS.public },
]),
);
} else {
devtool = 'eval-source-map';
plugins.push(
new webpack.NamedModulesPlugin(),
);
}
const config = {
devtool,
context: PATHS.root,
entry: [
PATHS.src,
],
output: {
path: PATHS.public,
filename: 'bundle.js',
publicPath: '/',
},
plugins,
module: {
rules: [
{
test: /\.js$/,
include: PATHS.src,
loader: 'babel-loader',
},
{
test: /\.css$/,
include: PATHS.src,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: {
loader: 'css-loader',
options: {
modules: true,
localIdentName: '[name]_[local]__[hash:base64:5]',
},
},
}),
},
{
test: /\.svg$/,
include: PATHS.src,
use: [
{ loader: 'url-loader', options: { limit: 10000 } },
],
},
{
test: /\.png$/,
include: PATHS.src,
use: [
{ loader: 'url-loader', options: { limit: 65000 } },
],
},
{
test: /\.(woff|woff2)$/,
include: PATHS.fonts,
loader: 'url-loader',
options: {
name: 'font/[hash].[ext]',
limit: 50000,
mimetype: 'application/font-woff',
},
},
],
},
resolve: {
modules: [
PATHS.src,
'node_modules',
],
alias: {
root: PATHS.root,
},
},
};
export default config;
/webpack.isomorphic.settings.json
{
"server": {
"input": "./src/server.js",
"output": "./build/dist/server.js"
}
}
/webpack.isomorphic.client.babel.js
import { client } from 'universal-webpack/config';
import settings from './webpack.isomorphic.settings.json';
import configuration from './webpack.config';
export default client(configuration, settings);
/webpack.isomorphic.server.babel.js
import { server } from 'universal-webpack/config';
import settings from './webpack.isomorphic.settings.json';
import configuration from './webpack.config';
export default server(configuration, settings);
/src/fetcher.js
import 'isomorphic-fetch';
// TODO: Update this when someone releases a real, production-quality solution
// for handling universal rendering with Relay Modern. For now, this is just
// enough to get things working.
class FetcherBase {
constructor (url) {
this.url = url;
}
async fetch (operation, variables) {
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: operation.text, variables }),
});
return response.json();
}
}
export class ServerFetcher extends FetcherBase {
constructor (url) {
super(url);
this.payloads = [];
}
async fetch (...args) {
const i = this.payloads.length;
this.payloads.push(null);
const payload = await super.fetch(...args);
this.payloads[i] = payload;
return payload;
}
toJSON () {
return this.payloads;
}
}
export class ClientFetcher extends FetcherBase {
constructor (url, payloads) {
super(url);
this.payloads = payloads;
}
async fetch (...args) {
if (this.payloads.length) {
return this.payloads.shift();
}
return super.fetch(...args);
}
}
/src/router.js
import queryMiddleware from 'farce/lib/queryMiddleware';
import createRender from 'found/lib/createRender';
import makeRouteConfig from 'found/lib/makeRouteConfig';
import Route from 'found/lib/Route';
import { Resolver } from 'found-relay';
import React from 'react';
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
// static
import CorePage from 'core/components/CorePage';
import LoadingComponent from 'core/components/LoadingComponent';
import ErrorComponent from 'core/components/ErrorComponent';
import HomePage from 'home/components/HomePage';
import NotFound from 'not-found/components/NotFoundPage';
// user
import UserContainer from 'user/containers/UserContainer';
import UserContainerQuery from 'user/queries/UserContainerQuery';
export const historyMiddlewares = [queryMiddleware];
export function createResolver (fetcher) {
const environment = new Environment({
network: Network.create((...args) => fetcher.fetch(...args)),
store: new Store(new RecordSource()),
});
return new Resolver(environment);
}
export const routeConfig = makeRouteConfig(
<Route path={'/'} Component={CorePage}>
<Route Component={HomePage} />
<Route
path={'user/:userId'}
Component={UserContainer}
query={UserContainerQuery}
/>
<Route path={'*'} component={NotFound} />
</Route>,
);
export const render = createRender({
renderPending: _ => <LoadingComponent />,
renderError: error => {
console.error(`Relay renderer ${error}`);
return <ErrorComponent />; // renderArgs.retry?
},
});
/src/index.js
import BrowserProtocol from 'farce/lib/BrowserProtocol';
import createInitialFarceRouter from 'found/lib/createInitialFarceRouter';
import React from 'react';
import ReactDOM from 'react-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { AppContainer } from 'react-hot-loader';
import { ClientFetcher } from './fetcher';
import { createResolver, historyMiddlewares, render, routeConfig } from './router';
injectTapEventPlugin();
(async () => {
// eslint-disable-next-line no-underscore-dangle
const fetcher = new ClientFetcher('/graphql-api', window.__RELAY_PAYLOADS__);
const resolver = createResolver(fetcher);
const Router = await createInitialFarceRouter({
historyProtocol: new BrowserProtocol(),
historyMiddlewares,
routeConfig,
resolver,
render,
});
const rootRender = Component => {
ReactDOM.render(
<AppContainer>
<Component resolver={resolver} />
</AppContainer>,
document.getElementById('root'),
);
};
rootRender(Router);
})();
I had some legacy code for a component that was referencing context.router.isActive from when I was using react-router-relay. Getting rid of that cleared up the problem

Resources