Guides & Tutorials

Creating and Editing Files with Angular Schematics

Guides & Tutorials

Creating and Editing Files with Angular Schematics

Welcome to the journey of creating Netlify schematics for Angular projects. If this is your first post, I'm very happy to have you :). Also, you can find more information about the Netlify schematics project in the other posts in this series:

In this post that you're looking at right now, part two of this series, we are going to do a few things:

  1. generate a blank schematic using the schematic CLI
  2. add ng-add functionality
  3. build out a schema for configuration information
  4. create two configuration files
  5. edit or create a .gitignore file

Let's go!

Generating a Schematic

The Schematics CLI tool will help us create a schematics skeleton, manage, and test the schematics. To create a basic schematic skeleton and name it we'll run this command:

schematics blank --name=my-schematic

That schematics command will create this file structure:

|- .gitignore
|- .npmignore
|- README.md
|- package.json
|- package-lock.json
|- tsconfig.json
|- src
  |- collections.json
  |- <schematic-name>
    |- index.ts
    |- index_spec.ts

🐙 Go to this commit on the Netlify schematics repo to check out what's in these files.

Setting Up ng add Functionality

The collection.json file has all the information about the schematic. It first points to a schema setup that lives inside the project. Then it has a schematics object where we'll add the different schematics we create. The schematics CLI skeleton project will list the schematic name (if given one), a default description, and a factory that will point to a function inside the project's index.ts file.

For this project I want to utilize the ng add functionality so the user will be able to write ng add netlify-schematics to install and run these schematics in their project. To do this I renamed the schematic from the project name to ng-add like so:

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "ng-add": {
      "description": "An ✨ awesome Netlify addition to your 💯 NG project.",
      "factory": "./netlify-schematics/index#netlifySchematics"
    }
  }
}

🐙 You can check out the changes to the collection.json file in this commit.

Making the Schema

Since we know what information we need to gather from the user in order to create the configuration files, we can create a schema file to hold those variables. To break it down here are the two files we are going to make and the information we need in each:

The schema file is where we can define variables or enumerated data types using the TypeScript interface. This file will live inside the schematics directory and be named schema.json (you can name it whatever you like as long as you reference it correctly in your collection.json file). So, in the Netlify schematics project the path is src/netlify-schematics/schema.json.

Here's a snippet of the netlify-schematics schema (you can check out the whole file here:

{
  "$schema": "http://json-schema.org/schema",
  "id": "NetlifySchematicsNgAdd",
  "title": "ng add schematic",
  "type": "object",
  "description": "Creates Netlify config files and edits the gitignore file 🔒",
  "properties": {
    "command": {
      "type": "string",
      "description": "Default build command.",
      "x-prompt": "What is 🧱 your project's build command? (maybe `ng build --prod`)"
    },
    "publish": {
      "type": "string",
      "description": "The project directory.",
      "x-prompt": "What 🗂 directory holds you built project? (maybe `dist/project-name`)"
    },
    ...
  }
}

Let's break each of part of this down.

  • $schema – this keyword declares that this JSON is a schematic and declares the JSON schema format this schema is written against.
  • id – an identifying name.
  • title – a human-readable description.
  • type – the form in which the properties will be delivered, in this case my properties are an object (this is common).
  • description – another descriptor for more details of what this code does.

properties

This properties object is going to get its own section because it needs its own set of bullets 😁. This is an object defining the available options for the schematic.

  • type – is the data type of this value.
  • description – describes what this property is or is used for.
  • x-prompt – this is where user interaction comes into play, you can learn more in the docs but basically, for this schematic, we're going to use the short form and just ask the user the question listed here and use their answer as this properties value.

Here is a screen capture to show you what the prompts will look like:

prompts output

Creating a New File

With all the user information we need we can now create the files to hold that information. Inside of the src/<schematic-name>/index.ts file is where we do the tree manipulation to edit the project that runs these schematics.

To recap, for the v1 of this schematic we'll add a Netlify config file, netlify.toml, and a file that will contain more sensitive project and account information, netlifyConfg.json. Since we don't want that sensitive data to see the light of day we'll also add it to the .gitignore file if it exists or create a .gitignore file listing the netlifyConfig.json file.

At the top of the file, options is already passed in from when we generated this schematic. It is referencing our schema thanks to SchematicContext. Each schematic runs in a context which is an object that provides access to utility functions and metadata, here, that's SchematicContext.

import { Rule, SchematicContext, Tree } from "@angular-devkit/schematics";

export function netlifySchematics(options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
  ...

The next step is to create variables that will hold the data for each file. This will make the file creation process look much cleaner.

const netlifyConfigData = `[build]\n  publish = "${options.publish}"\n  command = "${options.command}"`;
const privateNetlifyConfigData = {
  apiId: options.apiId,
  accessToken: options.accessToken,
  projectName: options.projectName
};

For the file creations we'll be using tree.create() which transforms the existing tree into a tree that now has that file.

tree.create("/netlify.toml", netlifyConfigData);
tree.create(
  "/netlifyConfig.json",
  JSON.stringify(privateNetlifyConfigData, null, 2)
);

Inside tree.create() we pass two parameters: a string filename and the contents we want in the file. For the first config file we just pass the variable we created (netlifyConfigData) and for the second we pass the stringified version of privateNetlifyConfigData.

🐙 You can check out the full index.ts file for netlify-schematics here.

Checking And Editing A File

We want to add the file with the sensitive information to the project's .gitigore file so it won't be pushed up to the user's git repository in case it's public. First, we'll check if the .gitignore file is inside the users's tree using the tree.exist() function, passing in the file name. If it doesn't, we immediately make one with the netlifyConfig.json file as the content. Then we return the tree since we're done changing things with this schematic.

if (!tree.exists("/.gitignore")) {
  tree.create("/.gitignore", "netlifyConfig.json");
  return tree;
}

If it does exist we create a variable called gitIgnoreBuffer and assign the contents of the .gitignore file. To do this we'll use the tree.read() function passing in the file name (like we did with tree.exist). If the buffer isn't empty we'll make yet another variable called newGitIgnore. This variable will be assigned to the a stringified version of gitIgnoreBuffer, a new line, plus the name of the file we want to start ignoring, netlifyConfig.json. The last step of this process is to use tree.overwrite() passing in the file we're overwriting, .gitignore, and what we're overwriting with, newGitIgnore. Now that we're done with that we can return tree since we have nothing left to change.

That was a lot of words! Here's what the code looks like:

let gitIgnoreBuffer = tree.read("/.gitignore");
if (gitIgnoreBuffer != null) {
  let newGitIgnore = `${gitIgnoreBuffer.toString()}\nnetlifyConfig.json`;
  tree.overwrite("/.gitignore", newGitIgnore);
}
return tree;

🐙 Here's a link to the index.ts file in full.

Function Recap

That was a lot of functions so I just want to write them here for my own future reference. Oh, I mean, obvioulsy you're welcome to them too 😘.

  • tree.create("filename", content): to create a new file in the tree
  • tree.exists("filename"): check if a file exists
  • tree.read("filename"): read the contents of a file
  • tree.overwrite("filename", "new content"): change the contents of a file with the provided content

Try It Out

To see how this works we first need to build out the project by running the command:

ng run build

Once it's built we can use the schematics CLI to walk through how the schematic works.

schematics .:ng-add --dry-run=false

You can learn more about this command in the next post on how to test and publish schematics. After running the schematic we'll have the two new files and an edit to the .gitignore file. Yay!

list of files and the .gitignore file

You've Got Files

Now you know how to add and edit files with schematics. Remember, with great power comes great testing. That's how the quote goes, right? Anyhoo, to check out how to test what we've created so far you can mosey on over to the next post I made covering just that! Have something that you wish this schematic did for your Angular project? Let's chat about it in the Netlify Community. In the meantime, happy coding! 👩🏻‍💻

Resources for the Road

Keep reading

Recent posts

Book cover with the title Deliver web project 10 times faster with Jamstack enterprise

Deliver web projects 10× faster

Get the whitepaper