angularazure-devopsyamlnpm-build

Angular build hangs without errors in DevOps pipeline


I have an Angular application, that is currently running without problems with Angular 13. Local build and deployment during DevOps YAML pipeline work well. We use an Npm@1 task to run "npm run build" (it's simply ng build) for our application here.

Now I've upgraded my application to Angular 17. On my local environment "npm run build" works fine without errors and the application is running. But during deployment with my DevOps pipeline I have problems. The build does either nothing (with Npm@1 task) or it seems to finish (with script task), but does not jump to the next pipeline step.

What did I try until now:

First I've used the same pipeline code like before with the Npm@1 task. Result: I see a black screen inside Devops and no output

      - task: Npm@1
        displayName: Build Angular Client
        inputs:
          command: custom
          workingDir: 'my directory'
          customCommand: 'run build'

At the moment I do my "npm run build" with a script task. I see the output (all generated bundles and so on), I see "Build at " like on my local machine. But the pipeline doesn't finish this script task and does not go to the next one. I also installed Angular CLI, Typescript and NodeJS before my build:

      - task: UseNode@1
        displayName: Install NodeJS 20.11.1
        inputs:
          version: '20.11.1'
      
      - script: |
          npm install -g @angular/cli@17.3.5
        displayName: Install Angular CLI 17.3.5

      - script: |
          npm install -g typescript@5.4.5
        displayName: Install Typescript 5.4.5

      - script: |
          npm ci
        displayName: Install Packages for App
        workingDirectory: 'my directory'

      - script: |
          npm run build
        env:
          NODE_OPTIONS: '--max-old-space-size=4096'
        displayName: Build App
        workingDirectory: 'my directory'

When I run the generated cmd File on the agent server, the Angular Build runs without problems.

What I recognized: The order of the output from "npm run build" is different on my local machine and on DevOps. It seems to be a little bit mixed up on DevOps: Local machine:

- Generating browser application bundles (phase: setup)...
√ Browser application bundle generation complete.
√ Browser application bundle generation complete.
- Copying assets...
√ Copying assets complete.
- Generating index html...
- Generating index html...
1 rules skipped due to selector errors:
  .k-input-label:dir(rtl) -> Unknown pseudo-class :dir
√ Index html generation complete.

Initial chunk files
...
Lazy chunk files
... (many lazy chunk files)
Build at: 2024-05-08T06:55:22.000Z - Hash: eeeb7d737af11c89 - Time: 280211ms

Warning: bundle initial exceeded maximum budget. Budget 1.00 MB was not met by 4.49 MB with a total of 5.49 MB.

DevOps:

- Generating browser application bundles (phase: setup)...
√ Browser application bundle generation complete.
√ Browser application bundle generation complete.
- Copying assets...
√ Copying assets complete.
- Generating index html...
- Generating index html...
1 rules skipped due to selector errors:
  .k-input-label:dir(rtl) -> Unknown pseudo-class :dir
√ Index html generation complete.

Initial chunk files
...
Lazy chunk files
... (only 4 lazy chunk files)

Warning: bundle initial exceeded maximum budget. Budget 1.00 MB was not met by 4.49 MB with a total of 5.49 MB.

... (the rest of the lazy chunk files)

Build at: 2024-05-08T06:55:22.000Z - Hash: eeeb7d737af11c89 - Time: 280211ms

I've also tried different versions of Angular CLI, Typescript, NodeJS with global install and without. And also we tried to set more space with NODE_OPTIONS.

Here is my code from my Angular project:

My ts.config File in Angular 17:

{
  "compileOnSave": false,
  "esModuleInterop": true,
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "ES2022",
    "module": "ES2022",
    "lib": [
      "ES2022",
      "dom"
    ],
    "types": [
      "jasmine",
      "jquery",
      "node"
    ],
    "useDefineForClassFields": false
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

My package.json:

{
  "name": "my-app",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "concurrently --kill-others \"ng serve\" \"npm run prettier-watch\"",
    "build": "ng build",
  },
  "private": true,
  "dependencies": {
    "@angular-builders/custom-webpack": "^17.0.2",
    "@angular/animations": "^17.3.5",
    "@angular/common": "^17.3.5",
    "@angular/compiler": "^17.3.5",
    "@angular/core": "^17.3.5",
    "@angular/forms": "^17.3.5",
    "@angular/localize": "^17.3.5",
    "@angular/platform-browser": "^17.3.5",
    "@angular/platform-browser-dynamic": "^17.3.5",
    "@angular/router": "^17.3.5",
    "@asymmetrik/ngx-leaflet": "~17.0.0",
    "@progress/kendo-angular-buttons": "~15.1.0",
    "@progress/kendo-angular-charts": "~15.1.0",
    "@progress/kendo-angular-common": "~15.1.0",
    "@progress/kendo-angular-dateinputs": "~15.1.0",
    "@progress/kendo-angular-dialog": "~15.1.0",
    "@progress/kendo-angular-dropdowns": "~15.1.0",
    "@progress/kendo-angular-editor": "~15.1.0",
    "@progress/kendo-angular-excel-export": "~15.1.0",
    "@progress/kendo-angular-grid": "~15.1.0",
    "@progress/kendo-angular-icons": "~15.1.0",
    "@progress/kendo-angular-indicators": "~15.1.0",
    "@progress/kendo-angular-inputs": "~15.1.0",
    "@progress/kendo-angular-intl": "~15.1.0",
    "@progress/kendo-angular-l10n": "~15.1.0",
    "@progress/kendo-angular-label": "~15.1.0",
    "@progress/kendo-angular-layout": "~15.1.0",
    "@progress/kendo-angular-menu": "~15.1.0",
    "@progress/kendo-angular-navigation": "~15.1.0",
    "@progress/kendo-angular-pdf-export": "~15.1.0",
    "@progress/kendo-angular-popup": "~15.1.0",
    "@progress/kendo-angular-progressbar": "~15.1.0",
    "@progress/kendo-angular-toolbar": "~15.1.0",
    "@progress/kendo-angular-treeview": "~15.1.0",
    "@progress/kendo-angular-upload": "~15.1.0",
    "@progress/kendo-angular-utils": "~15.1.0",
    "@progress/kendo-data-query": "~1.7.0",
    "@progress/kendo-drawing": "~1.19.0",
    "@progress/kendo-font-icons": "~2.0.0",
    "@progress/kendo-licensing": "~1.3.1",
    "@progress/kendo-svg-icons": "~2.0.0",
    "@progress/kendo-theme-default": "~7.2.0",
    "@ungap/structured-clone": "^0.3.4",
    "ajv": "~8.8.2",
    "angular-i18next": "~17.0.1",
    "bootstrap": "~3.4.1",
    "concurrently": "~7.2.1",
    "dompurify": "^2.3.9",
    "hammerjs": "^2.0.0",
    "i18next": "~23.11.2",
    "i18next-http-backend": "~1.3.2",
    "jquery": "~3.6.0",
    "jquery-param": "~1.2.3",
    "leaflet": "^1.9.3",
    "lodash.isequal": "^4.5.0",
    "loglevel": "~1.8.0",
    "rxjs": "~6.6.0",
    "tslib": "~2.3.0",
    "webpack": "^5.91.0",
    "zone.js": "~0.14.4"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "^17.3.5",
    "@angular/cli": "^17.3.5",
    "@angular/compiler-cli": "^17.3.5",
    "@angular/elements": "^17.3.5",
    "@babel/core": "~7.24.3",
    "@compodoc/compodoc": "~1.1.18",
    "@types/dompurify": "^2.3.3",
    "@types/jasmine": "~3.8.0",
    "@types/jquery": "~3.5.13",
    "@types/jquery-param": "~1.0.2",
    "@types/leaflet": "^1.9.0",
    "@types/lodash.isequal": "^4.5.6",
    "@types/node": "~12.11.1",
    "@types/ungap__structured-clone": "~0.3.0",
    "@typescript-eslint/eslint-plugin": "~7.7.1",
    "@typescript-eslint/parser": "~7.7.1",
    "@webcomponents/custom-elements": "~1.5.0",
    "babel-loader": "~9.1.3",
    "eslint": "~8.56.0",
    "fast-xml-parser": "^4.2.4",
    "jasmine-core": "~3.8.0",
    "karma": "~6.3.0",
    "karma-chrome-launcher": "~3.1.0",
    "karma-coverage": "~2.0.3",
    "karma-jasmine": "~4.0.0",
    "karma-jasmine-html-reporter": "~1.7.0",
    "karma-junit-reporter": "^2.0.1",
    "onchange": "~7.1.0",
    "playwright": "~1.25.2",
    "prettier": "2.6.2",
    "typescript": "^5.4.5",
    "wait-on": "^7.2.0",
    "webpack-bundle-analyzer": "^4.10.2"
  }
}

And my Angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {
    "analytics": false
  },
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "MyApp": {
      "projectType": "application",
      "schematics": {
        "@schematics/angular:component": {
          "style": "less"
        },
        "@schematics/angular:application": {
          "strict": true
        }
      },
      "root": "",
      "sourceRoot": "src",
      "prefix": "...",
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "./extra-webpack.config.js"
            },
            "allowedCommonJsDependencies": [
              "lodash.isequal",
              "leaflet",
              "dompurify"
            ],
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "inlineStyleLanguage": "less",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              {
                "glob": "**/*",
                "input": "serviceworker",
                "output": "./"
              },
              "src/web.config"
            ],
            "styles": [
              "node_modules/leaflet/dist/leaflet.css",
              "src/styles/bootstrap/_index.less",
              "src/styles/kendo/_index.scss",
              "src/styles/.../_index.less",
              "src/styles.scss",
              "node_modules/@progress/kendo-font-icons/dist/index.css"
            ],
            "stylePreprocessorOptions": {
              "includePaths": [
                "src/styles/global"
              ]
            },
            "scripts": [
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/jquery-param/jquery-param.min.js",
              "src/assets/signalr/jquery.signalR-2.1.2.js",
              "src/assets/signalr/hubs.js",
              "src/assets/pdfforms/pdfform.js",
              "src/assets/pdfforms/minipdf.js",
              "src/assets/pdfforms/pako.min.js"
            ]
          },
          "configurations": {
            "production": {
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "1mb",
                  "maximumError": "10mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "2kb",
                  "maximumError": "4kb"
                }
              ],
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "outputHashing": "bundles",
              "namedChunks": true
            },
            "development": {
              "buildOptimizer": false,
              "optimization": false,
              "vendorChunk": true,
              "extractLicenses": false,
              "sourceMap": true,
              "namedChunks": true,
              "outputHashing": "none"
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          ...
        },

      }
    }
  }
}

My extra-webpack.config.js

const { readdirSync, readFileSync, writeFileSync } = require("fs");
const path = require("path");
const config = require('./angular.json');

const angularDir = path.resolve(__dirname, config.projects.MyApp.architect.build.options.outputPath);
const swDir = path.resolve(__dirname, "serviceworker/");
const swTemplatePath = path.resolve(swDir, "sw-constants-template.js");

module.exports = {
  plugins: [
    {
      apply: (compiler) => {
        compiler.hooks.afterEmit.tap("Generate sw-constants.js", () => {
          const swTemplate = readFileSync(swTemplatePath, "utf8");
          const files = readdirSync(angularDir);
          writeFileSync(
            path.resolve(swDir, "sw-constants.js"),
            swTemplate
              .replace(/common\..+\.js/g, files.find(file => /common\..+\.js$/.test(file)) ?? "common.js")
              .replace(/main\..+\.js/g, files.find(file => /main\..+\.js$/.test(file)) ?? "main.js")
              .replace(/scripts\..+\.js/g, files.find(file => /scripts\..+\.js$/.test(file)) ?? "scripts.js")
              .replace(/polyfills\..+\.js/g, files.find(file => /polyfills\..+\.js$/.test(file)) ?? "polyfills.js")
              .replace(/runtime\..+\.js/g, files.find(file => /runtime\..+\.js$/.test(file)) ?? "runtime.js")
              .replace(/op\..+\.js/g, files.find(file => /op\..+\.js$/.test(file)) ?? "op.js")
              .replace(/spv\..+\.js/g, files.find(file => /spv\..+\.js$/.test(file)) ?? "spv.js")
              .replace(/styles\..+\.css/g, files.find(file => /styles\..+\.css$/.test(file)) ?? "styles.css"),
            "utf-8"
          );
        })
      }
    }
  ]
};

Do you have any ideas what's going wrong here?


Solution

  • I found the problem: we use a custom-builder for our angular app ("builder": "@angular-builders/custom-webpack:browser") which works fine for angular 13. But has problems with angular 17 in our case. When I remove it and use the default builder ("@angular-devkit/build-angular:browser"), my "npm run build" process will not hang anymore. It works now on my local machine and in my Devops pipeline!

    The next days there will be an update for this custom-webpack package, I hope, that this will perhaps fix the problem together with Angular 18. But for now I need to adapt my custom-webpack config or remove it and find another way to do some build steps in our project.