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 propertykey
with valuevalue
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 atfilePath
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 thesrc
directory to the project tree atdest
. This allows you to use a few modifiers from @angular-devkit/core by defaultcamelize
- 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 ifoptions.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 withreadJsonFile
). 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 (eitherangular.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 callsgetWorkspace
thengetProjectFromWorkspace
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));
}
]);
}