5

Honestly, I find it so annoying that this keeps changing. I solved this very issue in an easlier version of Angular here:

Deploying Angular Universal to Azure

But now this is out of date. There is no server.js generated anymore, instead you have to amend your web.config to point the the main.js which sounds like an improvement. I updated my yaml to this:

pool:
  name: Azure Pipelines
steps:
- task: gittools.gitversion.gitversion-task.GitVersion@5
  displayName: GitVersion

- task: NodeTool@0
  displayName: 'Use Node 12.x'
  inputs:
    versionSpec: 12.x

- task: Npm@1
  displayName: 'npm install angular cli'
  inputs:
    command: custom
    verbose: false
    customCommand: 'install @angular/cli -g'

- task: Npm@1
  displayName: 'npm install'
  inputs:
    verbose: false

- task: Npm@1
  displayName: 'npm build'
  inputs:
    command: custom
    verbose: false
    customCommand: 'run build:ssr'

- task: CopyFiles@2
  displayName: 'Copy dist files to staging'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/dist'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/app/dist'

- task: AzureRmWebAppDeployment@4
  displayName: 'Azure App Service Deploy: app-name'
  inputs:
    azureSubscription: 'Pay-As-You-Go (f61dc7cf-0ca2-4982-bbe7-9b6527c2962b)'
    WebAppName: r3plica
    packageForLinux: '$(Build.ArtifactStagingDirectory)/app'
    WebConfigParameters: '-Handler iisnode -NodeStartFile dist/app-name/server/main.js -appType node'

And that should be it, but ofcourse, it's not that simple. Now I am getting an error if I run node dist/app-name/server/main.js. It's returning this:

ReferenceError: Blob is not defined at createBase64WorkerFactory (D:\home\site\wwwroot\dist\app-name\server\main.js:1:1418371)

So I did a bit of looking around and someone suggested I install npm install --save-dev blob-polyfill which I did and then edit the server.ts file:

import { Blob } from 'blob-polyfill';

global['Blob'] = Blob;

But this does not have appeared to have done anything. The error still persists. Does anyone know what I have to do?


UPDATE

I decided to give this another go today. I ran npm build:ssr and copied the server & browser folders to a local webserver and ran node server/main.js and it complained that it couldn't find an index.html file at dist/my-project/browser/index.html which helped me. So I copied the entire dist folder to wwwroot and ran node dist/my-project/server/main.js and it worked.

So I updated my pipeline to do the same. I verified that it actually copies the whole dist folder and then I copy the web.config to the root. My web.config file looks like this:

<configuration>
    <system.web>
        <customErrors mode="Off" />
    </system.web>
    <system.webServer>
        <staticContent>
            <remove fileExtension=".woff2" />
            <mimeMap fileExtension=".woff2" mimeType="font/woff2" />
        </staticContent>
        <handlers>
            <!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
            <add name="iisnode" path="dist/my-project/server/main.js" verb="*" modules="iisnode"/>
        </handlers>
        <httpErrors errorMode="Detailed"></httpErrors>
    </system.webServer>
</configuration>

But when I try to load my website it just gives me an error:

HTTP Error 403.14 - Forbidden

A default document is not configured for the requested URL, and directory browsing is not enabled on the server.

which is really annoying. I had a look at the https://example.scm.azurewebsites.net and went to the debug console and typed node dist/my-project/server/main.js which came back with:

Node Express server listening on http://localhost:4000

So it should be working fine as far as I can tell. Does anyone have any idea why it isn't?

r3plica
  • 13,017
  • 23
  • 128
  • 290
  • Hi r3plica. I am having exactly same issues since a couple of days... could you make any progress or give me any hint on how to deploy our app? (Annoying is a very polite word to talk about this, BTW) ... – Juan Jun 05 '20 at 14:54

2 Answers2

3

So, once again I have managed to fix this. I poured hours into it and it was beginning to drive me mad. I decided to set up a local web server (iss) and try everything I could. In the end, it was this that saved me because iisnode was logging errors and I could see what was wrong.

If I kept the folder structure as it is when you do npm run build:ssr and update the webconfig to point to main.js like <add name="iisnode" path="dist/example-project/server/main.js" verb="*" modules="iisnode"/> you get an error similar to this:

Error: Failed to lookup view "index" in views directory "C:\inetpub\wwwroot\dist\example-project\server\dist\example-project\browser"

As you can see from the error, it's using a relative path from where main.js is. From this you can probably see how I fixed my issue.

I updated my tasks to include a new copy and I copy main.js to the root and update my web.config to this:

<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>.

For completeness, here is my full web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <webSocket enabled="false" />
        <handlers>
            <add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
        </handlers>
        <rewrite>
            <rules>
                <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
                    <match url="^main.js\/debug[\/]?" />
                </rule>
                <rule name="StaticContent">
                    <action type="Rewrite" url="public{REQUEST_URI}"/>
                </rule>
                <rule name="DynamicContent">
                    <conditions>
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
                    </conditions>
                    <action type="Rewrite" url="main.js"/>
                </rule>
                <rule name="Angular Routes" stopProcessing="true">
                    <match url=".*" />
                    <conditions logicalGrouping="MatchAll">
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="/index.html" />
                </rule>
            </rules>
        </rewrite>
        <security>
            <requestFiltering>
                <hiddenSegments>
                    <remove segment="bin"/>
                </hiddenSegments>
            </requestFiltering>
        </security>
        <httpErrors existingResponse="PassThrough" />
    </system.webServer>
</configuration>

And this is my xaml for azure:

pool:
  name: Azure Pipelines
steps:
- task: gittools.gitversion.gitversion-task.GitVersion@5
  displayName: GitVersion

- task: NodeTool@0
  displayName: 'Use Node 12.x'
  inputs:
    versionSpec: 12.x
    checkLatest: true

- task: Npm@1
  displayName: 'npm install angular cli'
  inputs:
    command: custom
    verbose: false
    customCommand: 'install @angular/cli -g'

- task: Npm@1
  displayName: 'npm install'
  inputs:
    verbose: false

- task: Npm@1
  displayName: 'npm build'
  inputs:
    command: custom
    verbose: false
    customCommand: 'run build:ssr'

- task: CopyFiles@2
  displayName: 'Copy dist files to staging'
  inputs:
    SourceFolder: '$(Build.SourcesDirectory)/dist'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/dist'

- task: CopyFiles@2
  displayName: 'Copy web.config'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)/dist/example-project/browser'
    Contents: web.config
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: CopyFiles@2
  displayName: 'Copy main.js'
  inputs:
    SourceFolder: '$(Build.ArtifactStagingDirectory)/dist/example-project/server'
    Contents: main.js
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: AzureRmWebAppDeployment@4
  displayName: 'Azure App Service Deploy: example-project'
  inputs:
    azureSubscription: 'Your Subscription'
    WebAppName: 'example-project'
    packageForLinux: '$(Build.ArtifactStagingDirectory)'
    enableCustomDeployment: true
    RemoveAdditionalFilesFlag: true
r3plica
  • 13,017
  • 23
  • 128
  • 290
  • 1
    I have faced this issue and stuck past few days. can you pls share any idea. What is ur azure app service type is it windows based ASP.Net Stack 4.7 something or anything else. – Karnan Muthukumar Sep 17 '20 at 15:53
  • https://stackoverflow.com/questions/63932339/how-to-publish-angular-9-unviersal-with-net-core-application-in-azure-app-servi I have asked questions. Can you pls replyme. – Karnan Muthukumar Sep 17 '20 at 15:55
  • 2
    I did the same, but this doesn't work with lazy loaded modules (I got an error D:\home\site\wwwroot\main.js - D:\Program Files\iisnode\interceptor.js Error: Cannot find module './21.js') @r3plica did you any such issue? – Indraneel Pole Jan 08 '21 at 11:47
  • 1
    Recently I did yeah. I changed the "Copy main.js" task to include all scripts (Change Contents to ** instead of main.js). If you look in dist/{app-name}/server you will notice there are js files for all your lazy loaded modules – r3plica Jan 08 '21 at 13:21
0

To anyone facing these issues, I've just solved it and here is our solutions but there are few facts:

  • [Web.config] Node Context, I mean the Process Working Directory, works different in iisnode, PWD is the target file path, this means that if your main.js is within dist/server/main.js then the paths relative to browser won't be dist/browser/ but ../browser/
  • 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

Juan
  • 2,156
  • 18
  • 26
  • 1
    Isn't it really annoying to have to redo things for each release of Angular :/ I will give this a shot (I feel that deployment should be easy and even easier the later down the versions you go, not harder....) – r3plica Jun 10 '20 at 13:28
  • Well, once I understood few things it is becoming easier. In example, my app uses i18n and I had to build n times and deploy n versions, one per supported locale and now our build time has decreased a lot... I'd encourage Angular Dev team to consider that most of the projects that use SSR also need to use browser related APIs so that import statement that must be placed after global declarations should be reviewed... – Juan Jun 13 '20 at 10:48