Automating the build and deployment of an Angular application in the Azure DevOps (AzDO) ecosystem is as easy as any other code hosting service. AzDO has added task types to help deploy directly to Azure subscriptions. In this post, we will look at creating a build pipeline with caching, discuss deploying to Azure App Service, and pushing a container to the Azure Container Registry for use in AKS.

Building the Pipeline

AzDO does have a GUI to help set up a JavaScript-based project. The YAML generated is a great starting place for most applications. We noticed while running the generated build pipeline that the Node Package Manager (NPM) installation task was taking a significant amount of time, 15 minutes in some cases. The packages and versions have been decided at this point in development and were not expected to change often. AzDO allows for caching of files or artifacts during a build to be used in subsequent pipeline runs, saving the node modules folder and not installing if there are no changes. The package-lock.json file with its hash keys is the perfect file for checking if there have been changes.

The first part of the pipeline for installation and building is kept from the generated pipeline: install the latest node version, 16.14.0 is the latest stable version at the writing of this post; install the specific angular cli version globally needed for the application, 13.2.5; install packages from package.json file and install a library from a tar archive located in our repository. Finally, run ng build to create a dist folder of the angular application that can be deployed to the cloud.

steps:

- task: NodeTool@0
  inputs:
    versionSpec: '16.x'
  displayName: 'Install Node.js'

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

- task: Npm@1  
  displayName: 'npm install'  
  inputs:
    command: custom  
    customCommand: 'install file:custom-angular-lib.tgz --legacy-peer-deps'  
  
- task: Npm@1  
  displayName: Build  
  inputs: 
    command: custom  
    verbose: false  
    customCommand: 'run build'  

Next, we wanted to update our job with caching to improve the run time of the pipeline. We added the npm_config_cache variable and used a “Cache” task type to set up the npm install. Using an environment variable to a path under $(Pipeline.Workspace) ensures the cache is accessible from container and non-container jobs. The logic on the cache task attempts to restore the cache, if successful the npm clean install step will not need to run.

variables:
  npm_config_cache: $(Pipeline.Workspace)/.npm

... 

- task: Cache@2
  inputs:
    key: 'npm | "$(Agent.OS)" | package-lock.json'
    restoreKeys: |
        npm | "$(Agent.OS)"
    path: $(npm_config_cache)
  displayName: Cache npm
- script: npm clean install

Lastly, building the Angular project with the ng build command and publish the created dist folder to our AzDO artifacts for use in the next stage, deployment.

- task: Npm@1  
      displayName: Build  
      inputs:
        command: custom  
        customCommand: 'run build'  
      
    - task: CopyPublishBuildArtifacts@1  
      displayName: 'Copy Publish Artifact: angular-web-app'  
      inputs:  
        CopyRoot: /dist  
        Contents: '**'  
        ArtifactName: angular-web-app 
        ArtifactType: Container

TOP 10 REASONS TO CHOOSE YAML FOR YOUR NEXT AZURE DEVOPS CI/CD PIPELINE
Discover reasons to choose YAML for your next Azure DevOps Pipeline and the benefits of a unified development experience.

Deploying the Pipeline

We had two deployment ideas in mind: Azure Kubernetes Service, AKS, and Azure App Service. Deploying to App Service requires the archived build artifact and pushing it to the Azure Subscription. Alternatively, if we wanted to use our application in AKS the application is bundled into a Docker image.

Before either deployment option, a task to sign in to the Azure subscription is required. More information on setting up to deploy to Azure can be found in Microsoft’s documentation.

- task: AzureCLI@2
      displayName: Az Login
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Login to AZ"
          az login --name <login name>

Deploy to App Service

The Azure Web Apps task can be used to deploy the Angular application artifact directly to the App Service. The task takes the artifact and pushes it to the App Service.

- task: AzureWebApp@1
    displayName: 'Deploy Azure Web App : angular-app'
    inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        appName: angular-app
        appType: webAppLinux
        package: $(Pipeline.Workspace)/angular-web-app 

Container for ACR and AKS

In order to deploy to an AKS cluster, the Angular application will need to be containerized and deployed to the Azure Container Registry, ACR. A basic Nginx container that hosts the dist folder is a common Angular dockerizing practice. Using the dockerfile in the repository, the pipeline will build the image and then push it to the ACR in our AZ subscription. Once the container is pushed to the registry, it can be used in any container service like AKS. See the Microsoft documentation on how to push to an Azure Container Registry.

- task: AzureCLI@2
      displayName: Az Container Registry Login
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Login to AZ"
          az acr login --name <login name>

- task: AzureCLI@2
      displayName: Docker Build angular-app
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Build image with docker"
          docker build -t angular-app:latest -f Dockerfile .

    - task: AzureCLI@2
      displayName: Docker Tag and Push angular-app
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          docker image tag angular-app:latest <name><endpoint>/<repo>/angular-app:latest
          docker image tag angular-app:latest <name><endpoint>/<repo>/angular-app:<build id>
          docker image push <name><endpoint>/<repo>/angular-app:<build id>
          docker image push <name><endpoint>/<repo>/angular-app:latest

Azure Artifacts vs. Caching

Caching was chosen as a solution over creating an artifact to improve build time by reusing files from earlier pipeline runs. The Microsoft documentation recommends using pipeline caching specifically for reducing build time and reusing files from previous runs. On the other hand, a pipeline artifact should be used if a job needs the files produced in a previous job in order to succeed. Since the installation of the node modules could be done even if a pulled cache fails, it made more sense to use caching.

Conclusion

By using AzDO pipelines, we were successful in creating a build pipeline with caching while having the flexibility to deploy to Azure App Service or AKS. The caching of the node modules sped up our installation task from 15 minutes to 3 minutes. The retrieval of the node modules still takes time but is faster overall, and possibly more cost-effective. Using the artifacts would not have benefited us in the same manner. Finally, the built-in Azure CLI tasks allow us to either deploy the Angular application directly to App Services or to build a Docker container, which allows us to push the image up to our Azure Registry for use in AKS or to use the container elsewhere.

An AIS client recently started developing a design system of components commonly used throughout multiple Angular 9 applications. To that end, we decided to take a component-driven approach using Storybook JS, version 6.1. We developed individual base components that would be used in more complex components, and then we would use a combination of those complex components and base components in web pages.

We quickly discovered that though Storybook supports Angular and other frameworks, it is primarily documented for React development. We also found some other techniques that are useful for development and documentation.

Here’s an overview of what we’ll cover in this piece:

  • Development
    • Rebuilding and Watch Mode
    • Router
    • Testing Components that Rely on the Output of Other Components
    • Mocking Services
  • Documentation
    • JSDoc Tags
    • Quotation Marks in JSDoc
    • MDX or TS Documentation?
    • documentation.json

Development

Rebuilding and Watch Mode

When developing with Storybook locally with ‘npm run storybook,’ Storybook has a watch mode that monitors an application’s source code and automatically rebuilds the application, similar to Angular’s watch mode when using ‘ng serve.’

There are a few minor caveats to Storybook’s watch mode, however. If you make changes to any of Storybook’s configuration files in the ‘.storybook’ directory, these files are not rebuilt by the Storybook process. If changes are made to the ‘.storybook’ directory, halt the ‘npm run storybook’ process, and restart it. Similarly, if you add new assets such as images or fonts to your application’s assets folder, Storybook will not package these new assets until you restart the process.

Router

If developing a project that uses the Angular router, and attempt to import the AppModule into the ModuleMetadata of one of your stories, then you will see the following error:
Unhandled Promise rejection: No base HREF set. Please provide a value for the APP_BASE_HREF token or add a base element to the document. ; Zone: ; Task: Promise.then ; Value: Error: No base HREF set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.

The easiest way to avoid this is not to import any module that defines any routes inside it or any of its imported modules. Instead, directly import the components that your application relies on in the story, or specify another module to import that contains only your components and not your application:

export default {
  title: 'My Story Name',
  component: MyStoryComponent,
  argTypes: {

  },
  decorators: [
    moduleMetadata({
      declarations: [
        MyStoryComponent,
        MyDependentComponent
      ],
    }),
  ]
};

If you have components that include the Angular router tests, look at the “Mocking Services” section of this post.

Testing Components that Rely on the Output of Other Components

One component we were developing in our application was a Password Strength Meter, which changes its styles and colors based on the complexity of the password provided to the meter. It took this password as an input property on the component. At first, we allowed the Storybook website user to modify the input property directly in the component’s property list in the Storybook Controls addon. However, we decided this was an insufficient test.

We discovered that it was effortless to define a new Angular component in the .stories.ts file for the Password Strength Meter. You specify this the same way you’d describe any other Angular component, using the @Component decorator, and implementing ngOnInit. Since it is not exported in an Angular module, this component will not be included in the deployment. We defined the component’s Template HTML inline, including both a text input field and our Password Strength Meter. We managed any of the ngModel events in the definition of the component.

Within the same .stories.ts file, you can define separate templates for individual stories. In that way, we can have both a “No text field” story that tests the Password Strength Meter without a text field and an “Integration” story that tests the password strength meter integrated with a text field:

const NoTextFieldTemplate = (args: PasswordStrengthComponent) => ({
  props: args,
  component: PasswordStrengthComponent,
});
export const NoTextField = NoTextFieldTemplate.bind({});
NoTextFieldTemplate.args = {
  ...actionsData
};
const IntegrationTemplate = (args: PasswordStrengthTestComponent) => ({
  props: args,
  component: PasswordStrengthTestComponent,
});

export const IntegrationWithTextField = IntegrationTemplate.bind({});
IntegrationWithTextField.args = {
  ...actionsData
};

Mocking Service

In our application development, we ran into situations where we would need to develop components that used dependency injection to obtain both built-in Angular services like the router and our services.
Since we define an inline Angular module in our story definition using ‘moduleMetadata’, it’s simple enough to add a “providers” line and use the “useValue” property.
For example, let’s say we have a service called SessionService that stores the user’s local session data, including their username. That service has a method named “getUsername()” that a component relies on. Also, this component utilizes the router. Then, inside our “.stories.ts” file, we can define our inline module:

export default {
  title: 'Header',
  component: BannerComponent,
  argTypes: {

  },
  decorators: [
    moduleMetadata({
      declarations: [
        BannerComponent
      ],
      imports: [
        RouterModule.forRoot([])
      ]
      providers: [
        {
          provide: SessionService, useValue: {
           getUsername: () => {
             console.log(`SessionService.getUsername called`);
             return `user1`;
           },
          }
        },
        {provide: APP_BASE_HREF, useValue: '/'}
      ]
    }),

  ]
};

In this example, we can see a mock of the SessionService that provides a fixed value to getUsername and the APP_BASE_HREF value that the Angular router relies on.

Documentation

One of the most significant benefits of Storybook is the amount of documentation that can be generated. Storybook uses Compodoc to generate its documentation, but you do have some options for overriding the default documentation generated. Storybook also offers many additional properties, as explained in their documentation.

Compodoc uses JSDoc to allow developers to design documentation for components easily. However, there are some limitations, as well as some other options for documentation.

JSDoc Tags

Compodoc only supports a small number of JSDoc tags, as documented on their website: @returns, @ignore, @param, @link, and @example. By far, the most useful of these in Storybook is @ignore. The @ignore tag will remove the property from the Storybook Controls panel, which helps hide properties and functions that external components should not use. Storybook infers many of these other properties, and we can define code examples in either the .stories.ts file or the .stories.mdx file.

Quotation Marks in JSDoc

One of the first things we found when writing JSDoc for Storybook is that either Compodoc or Storybook does not escape quotation marks properly for displaying in web browsers. We had to use the HTML escape for apostrophes and quotes, “’” and “”” inside our JSDoc documentation to get around this issue.

MDX or TS Documentation?

MDX is a file format similar to Markdown provided as an option for Storybook documentation. We found that MDX is slightly harder to use for Angular developers, as it requires learning a new syntax based on React development. Despite this drawback, MDX gives the ability to provide complex Markdown formatting to the pages. When developing in Storybook, consider using MDX if you need an involved documentation page that does not fit the default Storybook documentation formatting style.

You can mix and match MDX with Typescript and define your stories in a TS file and import them in an MDX file if you need complex story definitions alongside complex markdown.
Often, TS documentation will be enough; we can use the various properties Storybook provides alongside JSDoc. However, for those times that we need a bit more customization, MDX files can provide what we need.

documentation.json

When Compodoc builds, it creates a large file in the root directory of the application called documentation.json. This file is created when Compodoc is run. This file does not need to be committed to git and can safely be included in a .gitignore file.

Conclusion

Storybook is a powerful tool used to develop and document components in Angular. Though its documentation is primarily written for React, its capabilities are equally strong with Angular and will help design document reusable components for a large application.