Angular Schematics from 0 to publishing your own library (IV)

Schematics are a great tool to standarize patterns, enforce best practices, and make sure repositories across large organizations, by automating certain implementations. We have extensively discussed this in previous posts, but now we will demonstrate this in practice.

Angular Schematics from 0 to publishing your own library (IV)

Schematics are a great tool to standarize patterns, enforce best practices, and make sure repositories across large organizations, by automating certain implementations. We have extensively discussed this in previous posts, but now we will demonstrate this in practice.

Introducing an established schematics library

This series of posts is proposing we create our own schematics based library and we publish it to `npm`. The problem I will propose, involves a very popular, heavily schematics based library, Scully.io.

If you've already checked Scully, you know that it is a static sites generator, that basically uses a headless chromium browser, spun up with Puppeteer, in order to pre-render pages and save them as static HTML, with all the inline assets included, in the dist/static folder.

Scully uses a collection of schematics to allow developers to quickly create content. For example we have add-blog and add-post. These two work in combination to quickly create a blog folder, and then successive blog posts from the CLI.

My problem to solve with schematics

Back in August I was a trainer at an Angular Schematics workshop with ngConfCo. I knew it was going to be a challenging workshop since it was 4 hours, fully online and there were 60+ registered attendees, all with different levels of expertise. In fact, the content you're reading, is a spin-off of the translation of the material for that workshop.

The problem was, I was not sure how to organize the content so I could make it available to all attendees, regardless of their connection speed, etc. The solution was rather obvious: since I was at an Angular conference, it had to be an Angular site. Since I wanted a fast site, it had to be static. The solution was creating my content with Angular, pre-rendering it with Scully and publishing it on one of those amazing JAM stack platforms.

I could've used the blog and post schematics, but I decided to go ahead and extend the post schematic, to generate docs instead. So instead of generating a new document with

$ ng generate @scullyio/init:post
scullyio generate post schematic

And have to answer blog/post related prompts, I would type

$ ng generate add-doc:doc
generate schematic from angular app with angular-cli

And that would chain my custom schematic with my default values, to the add-post Scully schematic. For now, you can read how I created the docs module, and the side navigation and how I configured the routes for Scully here. We will later do that also with schematics. Let's now put our focus on this add-doc schematics extension, in a practical way.

Working on our custom schematic

You know how to do this already, so go ahead and create a new blank schematic, called add-doc

$ schematics blank --name=add-doc
generate a new blank schematic

Now let's open the index.ts for this schematic, and start writing our add-doc schematic.

Verifications not needed

Because this schematic will be chained to a Scully schematic, we don't need to verify if we are in an Angular workspace, though you are safe to do it, none the less. Although if you're going to be writing a lot of schematics, in a library, you may want to think about extracting the verifications to an independent schematic, to chain in the first place.

Define options

The first thing we want to do, is override the options for the Scully add-post schematic, that we will chain our schematic to.

// We want to override the blog default target folder of post, by docs
options.target = 'docs';
// we will use this target variable later
const target = tree.getDir(options.target);
options.title = options.name || 'doc-X';
options.description = 'doc description';
defining options overrides

It's important to understand that some of options won't work, unless Scully add-post function and schema, change a bit, as I proposed here.

At the time of writing this post and that code, Scully was in beta, and it has now released its first stable version. So stay tuned for updates!

The side navigation and the file path

Now this is an important aspect: we already established that it is a good idea to automatically add every new route to a navigation, to make them accessible. Adding them manually would be a hassle. Not something you want to do. As I already stated, we will include another schematic to this library in a future post, to show how to automate its creation, too. But for now, we have created it manually following the instructions here.

What we want to do, is keep those files in order. We could totally add a new metadata property called index for example, to set the order. But I decided I want that to be visible in the file path as well. And I do that by adding a numeric prefix to each file path generated.

  // Let's create an array to push the generated files to
      const indices: number[] = [];

      target.visit(file => {
        // Now let's just get the index of the last doc created to order the sidenav
        let fileName = basename(file);
        let fileIndex = parseInt(fileName.substring(0,3), 10);
        indices.push(fileIndex || 0o0);
      });

      if (indices.length !== 0) {
        let maxIndex = Math.max(...indices);
        let newIndex = ++maxIndex;
        let index = newIndex.toString();

        const _index = index.length === 1 ? `00${newIndex}` : index = `0${newIndex}`;

        // We increment index name so we have an ordered list for the sidenav
        // We want to make sure the mandatory name option of the post schematic is satisfied
        options.name = `${_index}${options.title}`;
      } else {
        // Even if the folder did not exist or was empty before, we need to satisfy this
        options.name = `000${options.title}`;
      }
custom function to order paths

Now this is mostly what this schematic is busy with, because the bulk of the file generation, is already done by the Scully add-post file, and we don't need to reinvent the wheel. All we need to do, is to chain our custom schematic, to the existing external Scully schematic.

externalSchematic('@scullyio/init','post', options)
    ])
chaining with and external schematic

Please keep in mind that, for this to work, Scully must be a dependency of your schematic package!

Remember that by default, all schematics run synchronously. That means that I can be certain that the order of execution of the schematics, is

add-doc => add-post

Working on the validation schema and prompts

We are not ready yet, there are few more things we need to do. Let's add a validation schema, to make sure we have the right data and prompts, to execute our schematic.

First of all, let's replace the any type for the options, for a custom type. Just as before, create a schema.ts file, and update it like this

/**
 * Using the same model for options as
 * Scully ng-add-blog schematic
 */
export interface Schema {
  /**
   * add the title for the post
   */
  name?: string;
  /**
   * add the title for the doc post
   */
  title?: string;
  /**
   * define the target directory for the new post file
   */
  target?: string;
  /**
   * define the file extension for the target file
   */
  extension?: string;
  /**
   * override the post description
   */
  description?: string;
}
Exporting the interface Schema

Again, remember that the description and title won't really take effect, unless the Scully schematic get updated.

Now let's create our schema.json file. We want to add the right prompts. These prompts will take over the add-post schema prompts and be the only ones showing in the terminal, as you will be able to verify later.

{
  "$schema": "http://json-schema.org/schema",
  "id": "@scullyio/init:post",
  "title": "Scully: Add a blog post schematic",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "add the title for the post",
      "x-prompt": "What's the title of this doc?",
      "default": "doc-X"
    },
    "title": {
      "type": "string",
      "description": "add the title for the doc",
      "default": "doc-X"
    },
    "target": {
      "type": "string",
      "description": "define the target directory for the new post file",
      "x-prompt": "What is the target folder for your docs?",
      "default": "docs"
    },
    "extension": {
      "type": "string",
      "description": "define the file extension for the target file",
      "default": "md"
    },
    "description": {
      "type": "string",
      "description": "use a meta data template file that's data will be added to the post",
      "x-prompt": "What is the description for this post?",
      "default": "document description"
    }
  },
  "required": ["name"]
}
Writing our validation schema, including the new properties and prompts

Let's now not forget to update the collection. First of all, we want to make sure to reference our validation schema. Second, we probably want to add an alias, to call our schematic with the single word, no dashes, doc

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "add-doc": {
      "description": "A blank schematic.",
      "factory": "./add-doc/index#addDoc",
      "aliases": ["doc"],
      "schema": "./add-doc/schema.json"
    }
  }
}
Update the collection with the schema reference and alias

We are ready to go!

Let's now put the whole code together. Don't forget to build all these changes!

import { Rule, SchematicContext, Tree, chain, externalSchematic } from '@angular-devkit/schematics';
// import { strings } from '@angular-devkit/core';
import { basename } from 'path';
import { Schema } from './schema';

// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function addDoc(options: Schema): Rule {
  return chain([
    chain([ (tree: Tree, _context: SchematicContext) => {
     
      // We want to override the blog default target folder of post, by docs
      options.target = 'docs';
      const target = tree.getDir(options.target);
      options.title = options.name || 'doc-X';
      options.description = 'doc description';

      // Let's create an array to push the generated files to
      const indices: number[] = [];

      target.visit(file => {
        // Now let's just get the index of the last doc created to order the sidenav
        let fileName = basename(file);
        let fileIndex = parseInt(fileName.substring(0,3), 10);
        indices.push(fileIndex || 0o0);
      });

      if (indices.length !== 0) {
        let maxIndex = Math.max(...indices);
        let newIndex = ++maxIndex;
        let index = newIndex.toString();

        const _index = index.length === 1 ? `00${newIndex}` : index = `0${newIndex}`;

        // We increment index name so we have an ordered list for the sidenav
        // We want to make sure the mandatory name option of the post schematic is satisfied
        options.name = `${_index}${options.title}`;
      } else {
        // Even if the folder did not exist or was empty before, we need to satisfy this
        options.name = `000${options.title}`;
      }
      return tree;
    },
    externalSchematic('@scullyio/init','post', options)
    ])
  ])
}

Running our schematic

In order to test this schematic (at least the file generation), create a new app in with the CLI, and link this package.

You will evidently need to have Scully installed as well! Other pre-requisites apply, too! Please go to the previous post, to review if your have all dependencies necessary to run schematics.

Now go to the terminal, and run

$ ng generate add-doc:doc

You will be prompted to select the default value or pass a title and target for your doc to be generated.

You can run it a few more times, and see how the index gets added, to order your paths. Please keep in mind that I only align lengths up to 3 digits. If you have more than 1K documents, it will break ;)

Unit testing your schematic

I know that by now you're wondering about testing your schematic, how is this done? There are specific methods to test schematics, so we will dedicate a chapter exclusively for that. Stay tuned!

And this is it! Let's meet again in the next and last issue of this series, to create the side navigation with schematics, and publish our library with ng-add support!

Repository

You can checkout this schematic here -> https://github.com/anfibiacreativa/add-doc/blob/master/README.md