I created this post to get some insights from the community.
A little while ago with the release of .NET Core 3.0 the usage of the well-known and widely used spa.UseSpaPre
To anyone facing this issues, I've just solved it and here is our solutions but there are few facts:
Consider that during deployment you will have to generate Web.config according to this new structure
-Handler iisnode -NodeStartFile dist/server/main.js -appType node
[server.ts] - Having that in mind consider also to set the browser path according to your runtime environment so that if you are in production it should be ../browser
[server.ts] - Order matters in server.ts. IF YOU FACE BROWSER API ISSUES it is because "import { AppServerModule } from './main.server';" MUST be placed AFTER domino declarations.
Here is a working example on a server.ts that is also using i18n redirections according to url requests with a locale string (now that I solved this i18n issues too it I can tell you that it worth to read the docs).
/***************************************************************************************************
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
*/
import { APP_BASE_HREF } from '@angular/common';
import '@angular/localize/init';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { existsSync } from 'fs';
import { join } from 'path';
import 'zone.js/dist/zone-node';
import { environment } from './environments/environment';
// THIS FIX MOST OF THE COMMON ISSUES WITH SSR:
// FIRST SET THE BROWSER PATH ACCORDING TO RUNTIME ENVIRONMENT
let browserPath;
if (environment.production) {
browserPath = '../browser';
} else {
browserPath = 'dist/browser';
}
const enDistFolder = join(process.cwd(), browserPath + '/en');
// Emulate browser APIs
const domino = require('domino');
const fs = require('fs');
const templateA = fs.readFileSync(join(enDistFolder, 'index.html')).toString();
const win = domino.createWindow(templateA);
console.log('win');
win.Object = Object;
console.log('Object');
win.Math = Math;
console.log('Math');
global['window'] = win;
global['document'] = win.document;
global['Event'] = win.Event;
console.log('declared Global Vars....');
/****************************************************/
/** NOTE THIS: I need to avoid sorting this line */
// USE CTRL+P -> SAVE WITHOUT FORMATTING
import { AppServerModule } from './main.server';
/****************************************************/
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
const server = express();
const indexHtml = existsSync(join(browserPath, 'index.original.html')) ? 'index.original.html' : 'index.html';
// 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', browserPath);
// Example Express Rest API endpoints
// server.get('/api/**', (req, res) => { });
// Serve static files from /browser
server.get('*.*', express.static(browserPath, {
maxAge: '1y'
}));
server.use('/robots.txt', express.static('/en/robots.txt'));
server.use('/ads.txt', express.static('/en/ads.txt'));
// THE ORIGINAL Universal Requests handler
// // // All regular routes use the Universal engine
// // server.get('*', (req, res) => {
// // res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
// // });
// OUR i18n REQUESTS HANDLER
// All regular routes use the Universal engine
server.get('*', (req, res) => {
// this is for i18n
const supportedLocales = ['en', 'es'];
const defaultLocale = 'es';
const matches = req.url.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
// check if the requested url has a correct format '/locale' and matches any of the supportedLocales
const locale = (matches && supportedLocales.indexOf(matches[1]) !== -1) ? matches[1] : defaultLocale;
res.render(`${locale}/index.html`, { req });
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app();
server.listen(port, () => {
console.log(`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 './main.server';
I still need to work a bit on this code and in our app (SSR and oauth issues, another funny topic) but I want to share it because it took us almost 20 deployments to fix these issues.
Final words: if you come here after an angular 8 migration I'll be glad to help you and give you nice hints but, honestly, follow the guide and read carefully the docs. Also, if you are using Azure DevOps pipelines, you should consider using an npm cache. Our as is large and we are now saving more than 12 minutes on each build process (That is a huge amount of time, isn't it?) Feel free to get in touch with me.
Juan
I'm struggling with netcore 3.1 and angular too when it comes to deploy the project on azure or anything.. did you find anything? Could you provide your startup file?
when I use dotnet publish, package.json is not copied to publish/ClientApp directory so the command used by spa.UseAngularCliServer()
fails or it just doesn't find /index.html.
for now, I run my project locally like this:
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "dev:ssr");
}
});