After upgrading my Angular 8 project to 9, and by that I mean that I upgraded practically everything to the latest, several things happened.
1.- webpack.config.js
file was renamed as .bak so it seems that using webpack is not the way anymore, and indeed if I try to re-activate the webpack build nothing happens, it creates the build but the script doesn't run. Running with node server.js
goes in and out without saying anything at all.
2.- Now the dist folder has no .js file in the root and the browser & server builds' folders are there like before, but even editing the web.config
file to instruct the iisnode to run server/main.js
instead of server.js
throws 500 all the time no matters what I do.
3.- The only thing that works at first is runnign locally
npm run dev:ssr
Which means that the reference to the view in the browser build is there but it runs like it should run from the root asking for dist/browser.
I have modified the build path and server build path in angular.json
to create the main.js file in the root of ./dist folder.
angular.json (outputPath are modified)
architect: {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/browser",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
...
},
...
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
...
},
...
}
Well, here comes the problem. The browser build generates ./dist/browser without problems and the server build indeed put the main.js file directly in ./dist but somehow it decided to delete the ./dist/browser folder and when running node main.js (web.config
pointing to main.js
) it can't find ./browser/index.html and of course not because the server build process managed to delete the browser folder.
The hacky solution, run a third build (the browser build again)
I hope this can be solved but to be honest this doesn't bother me at all, a computer runs the build, not me, but you know, it could be better and quicker if the server build proces doesn't delete de browser build folder.
Now it is deliciously deployed to Azure App Service directly from AppVeyor every time I deploy the selected build, no problems what soever.
package.json (Note the extra production build after the server build)
{
...
"scripts": {
...
"dev:ssr": "ng run Angular-Universal:serve-ssr",
"serve:ssr": "node dist/main.js",
"build:ssr": "npm run build:universal && ng run Angular-Universal:build:production",
"start:ssr": "npm run build:ssr && npm run serve:ssr",
"build:universal": "ng run Angular-Universal:build:production && ng run Angular-Universal:server:production",
"prerender": "ng run Angular-Universal:prerender"
},
...
}
Note that build:ssr
script is doing both builds and then the browser build again.
Also note that serve:ssr
script was modified to run dist/main.js
Finally the server.ts file had to be modified to request the view from ./browser/index.html or ./dist/browser/index.html depending on the folder structure of the target system. When deployed to Azure App Service main.js requests browser/index.html but if the project is served with npm run dev:ssr
or node main.js
in my local environment with runs on macOS it requests dist/browser/index.html. Anyways a simple if can jump this obstacle.
server.ts (Changes on the app function)
// The Express app is exported so that it can be used by serverless Functions.
export function app() {
const server = express();
let distFolder: string;
const path1 = join(process.cwd(), 'browser'); // Running from the deployed version (production)
const path2 = join(process.cwd(), 'dist/browser'); // Running from the source (macOS local development)
if (fs.existsSync(path1)) {
distFolder = path1;
} else if (fs.existsSync(path2)) {
distFolder = path2;
} else {
return null;
}
console.log('distFolder:', distFolder);
const indexHtml = fs.existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// 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);
// 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, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
return server;
}
Just in case you were wondering what about copying the web.config
file to the root of the dist
folder, well, since I am using AppVeyor for CI/CD I have modified the artifact creation command to include web.config
(from the repo source files directly into the .7z file)
And why not to include my web.config
?
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<httpRuntime enableVersionHeader="false" />
</system.web>
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Strict-Transport-Security" value="max-age=31536000"/>
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="DENY" />
<add name="X-XSS-Protection" value="1; mode=block" />
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
<webSocket enabled="false" />
<handlers>
<!-- Indicates that the main.js file is a node.js site to be handled by the iisnode module -->
<add name="iisnode" path="main.js" verb="*" modules="iisnode"/>
</handlers>
<rewrite>
<rules>
<rule name="HTTP to HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
<!-- Do not interfere with requests for node-inspector debugging -->
<rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
<match url="^main.js\/debug[\/]?" />
</rule>
<!-- All other URLs are mapped to the node.js site entry point -->
<rule name="DynamicContent">
<match url="^(?!.*login).*$"></match>
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true"/>
</conditions>
<action type="Rewrite" url="main.js"/>
</rule>
</rules>
<outboundRules>
<rule name="Add Strict-Transport-Security when HTTPS" enabled="true">
<match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" />
<conditions>
<add input="{HTTPS}" pattern="on" ignoreCase="true" />
</conditions>
<action type="Rewrite" value="max-age=31536000" />
</rule>
</outboundRules>
</rewrite>
<!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
<security>
<requestFiltering>
<hiddenSegments>
<remove segment="bin"/>
</hiddenSegments>
</requestFiltering>
</security>
<!-- Make sure error responses are left untouched -->
<httpErrors existingResponse="PassThrough" />
<!-- Restart the server if any of these files change -->
<iisnode watchedFiles="web.config;*.js;browser/*.*" />
</system.webServer>
</configuration>
I hope this could help others in my situation or at least useful for debate. No one likes to be scratching the head trying to make a successful deployment to an Azure App Service after upgrading to version 9. Please let me know if something can be improved, perhaps a build parameter to avoid browser build folder removal or whatever. Cheers.