@ockilson/schematics-utils

Schematic utilities for writing custom schematics

Usage no npm install needed!

<script type="module">
  import ockilsonSchematicsUtils from 'https://cdn.skypack.dev/@ockilson/schematics-utils';
</script>

README

@ockilson/schematics-utils

Schematics Utilities and Helpers

This is a collection of helper functions for working with Schematics

While offering a lot of powerful functionality the Schematics toolkit from angular-devkit/{core,schematics} are fairly low level so this wraps some high level re-usable functionality.

Dependencies

Handling dependencies when setting up a new tool is fairly common so there are a few functions to make it a touch easier.

  • addPackageJsonDependency(tree: Tree, dependency: NodeDependency): void - adds a dependency (either; dev, default, peer or optional) at the set version to the package.json at the root of the tree.
  • getPackageJsonDependency(tree: Tree, name: string): NodeDependency | null - returns a dependencies information from the package.json at the root of the tree.
  • removePackageJsonDependency(tree: Tree, dependency: DeleteNodeDependency): void - remove a dependency from the package.json at the root of the tree.

These utilities can be used in your own custom schematics by passing the tree and the dependency to modify.

There are also two wrapper functions that take the schematics options and an array of dependencies; these return a Rule so can be chained with other schematics easily

  • addDependenciesToPackageJson(options: any, deps: NodeDependency[] = []): Rule - loops through the dependencies, adds them to the package.json file at the root of the tree and then runs npm install.
  • removeDependenciesFromPackageJson(options: any, deps: DeleteNodeDependency[] = []): Rule - loops through the dependencies, removes them from the package.json file at the root of the tree and then runs npm install (has the same effect as running npm uninstall on each of the packages individually).
  • addScriptToPackageJson(options: any, key: string, value: any): Rule - adds a script to the scripts object at property key with value value in the package.json at the root of the tree.

Usage Example

Inside a custom schematic factory file...

const dependencies: NodeDependency[] = [
    {
        type: NodeDependencyType.Dev,
        version: '^23.6.0',
        name: 'jest'
    }
];

const removeDeps: DeleteNodeDependency[] = [
    {
        name: "karma",
        type: NodeDependencyType.Dev
    }
];

export function myCustomSchematic(_options: any): Rule {
    return (tree: Tree, context: SchematicContext) => {
        return chain([
            removeDependenciesFromPackageJson(_options, removeDeps),
            addDependenciesToPackageJson(_options, dependencies),
            customDependencyRule(_options)
        ]);
    }
}

export function customDependencyRule(options: any) {
    return (tree: Tree, context: SchematicContext) => {
        const jestDep = getPackageJsonDependency(tree, 'jest');

        if(!jestDep) {
            throw new SchematicsException('Jest is not currently installed, you should fix that');
        }
        console.log(`Currently installed jest version is: ${jestDep.version}`);
        return tree;
    }
}

Files

Since this is primarily targetted at people using the @angular/cli two things that are fairly common are copying files from your custom schematic into the project directory and reading from json, so...

  • readJsonFile(tree: Tree, filePath: string): JsonAstObject - reads a json file at filePath in the project tree and converts to an AST representation.
  • addSchematicsFilesToProject(options: any, dest: string = '', src: string = './files', modifiers: object = {}): Rule - copies all files from the src directory to the project tree at dest. This allows you to use a few modifiers from @angular-devkit/core by default
    • camelize - Convert a string into camelCase (my-thing-is-cool => myThingIsCool)
    • dasherize - Convert a string into kebab-case (my thing is cool => my-thing-is-cool)
    • classify - Convert a string into PascalCase, handy for class names (my-component => MyComponent)
    • if-flat - Checks if options.flat is set, handy for working out paths You can use the last parameter to add additional modifiers as required.

Usage Example

import { strings } from "@angular-devkit/core";

export function myCustomSchematic(_options: any): Rule {
    return (tree: Tree, context: SchematicContext) => {
        return chain([
            addSchematicsFilesToProject(_options) // adds everything in `./files` to the root of the current tree
            addSchematicsFilesToProject(_options, 'scripts', './projectFiles', {
                strings.decamelize
            }) // adds everything in `./projectFiles` to `./scripts/` in the current tree and adds the additional `decamelize` modifier
        ]);
    }
}

Ast (abstract syntax tree)

Since Schematics is centered around modifying Ast objects which can be pretty tricky to work with there are a few utilities to make common tasks a little easier.

  • appendPropertyInAstObject(recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue, indent: number) - add a new property to an existing ast object, this could be used to add a new property to the package.json, tsconfig.json or angular.json (after reading the file with readJsonFile). It does no checking for existing properties.
  • insertPropertyInAstObjectInOrder(recorder: UpdateRecorder, node: JsonAstObject, propertyName: string, value: JsonValue, indent: number) - adds a new property to an existing ast object (as above) but in alphabetical order by property key.
  • appendValueInAstArray(recorder: UpdateRecorder, node: JsonAstArray, value: JsonValue, indent = 4) - add a new item to an existing ast array object. It does no checking for existing properties.
  • findPropertyInAstObject(node: JsonAstObject, propertyName: string): JsonAstNode | null - finds and returns a property from an ast object (or null if not found).

These are a little more low level but should help you modify basic objects a little easier.

Usage Example

export function customSchematic(_options: any) {
    return (tree: Tree, _context: SchematicContext) => {
        const configAst = readJsonFile(tree, 'tsconfig.json');
        const compilerOptionsAst = findPropertyInAstObject(tsSpecConfigAst, 'compilerOptions') as JsonAstObject;
        const recorder = tree.beginUpdate('tsconfig.json');

        if(!compilerOptionsAst) {
            // if compiler options don't exist add them
            appendPropertyInAstObject(
                recorder,
                configAst,
                "compilerOptions",
                {
                    types: ['jest']
                },
                2
            );
        }
        tree.commitUpdate(recorder);
    }
}

Workspace

The angular cli uses the concept of a workspace, configured via angular.json at your project root, for a lot of schematics you may want to modify this config or get information out of it, the following are provided to make these easier.

  • getWorkspacePath(host: Tree): string - return the path to the workspace configuration file (either angular.json or .angular.json are valid).
  • getWorkspace(host: Tree): WorkspaceSchema - load the workspace configuration.
  • addProjectToWorkspace(workspace: WorkspaceSchema, name: string, project: WorkspaceProject): Rule - add a new project to a given workspace, will fail if project with that name already exists.
  • getProjectFromWorkspace(workspace: WorkspaceSchema, name: string): WorkspaceProject - returns a specific project from the given workspace.
  • updateProjectInWorkspace(workspace: WorkspaceSchema, name: string, project: WorkspaceProject): Rule - update an existing project in a given workspace.
  • getProject(host: Tree, name: string): WorkspaceProject - short cut that calls getWorkspace then getProjectFromWorkspace on the result.
  • getProjectRootPath(host: Tree, name: string): string - returns the path to the root of the given project, this will be the workspace root when only one project exists.
  • getProjectPath(host: Tree, name: string): string - returns the path to the project code root, for applications this will be <root>/src/app and for libraries it will be <root>/src/lib.
  • getDefaultProject(host: Tree): string - returns the name of the default project in the current workspace.

A lot of this functionality is better covered by extending angulars existing schematics (for example for adding a new project).

Usage Example

export function customSchematic(_options: any) {
    return chain([
        (tree: Tree, _context: SchematicContext) => {
            const workspace = getWorkspace(tree);

            // update the default collection the workspace should use for schematics
            workspace.cli = {
                ...workspace.cli,
                defaultCollection: "."
            };

            tree.overwrite(getWorkspacePath(tree), JSON.stringify(workspace, null, 2));
            return tree;
        },
        (tree: Tree, _context: SchematicContext) => {
            const projectName = getDefaultProject(tree);
            const workspace = getWorkspace(tree);
            const project = getProjectFromWorkspace(workspace, projectName);

            if(!project.architect || !project.architect.test) {
                throw new SchematicsException(`No project architect configuration available for project ${projectName}`);
            }

            project.architect.test.builder = "@angular-builders/jest:run";

            return branchAndMerge(updateProjectInWorkspace(workspace, projectName, project));
        }
    ]);
}