Skip to content

Instantly share code, notes, and snippets.

@twittwer
Last active June 10, 2020 18:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save twittwer/d1adb85f873b02b14b6f52d2aa9ec4f3 to your computer and use it in GitHub Desktop.
Save twittwer/d1adb85f873b02b14b6f52d2aa9ec4f3 to your computer and use it in GitHub Desktop.
Workspace Schematic For Nx Libraries
import { experimental } from '@angular-devkit/core';
import { chain, externalSchematic, Rule, Tree } from '@angular-devkit/schematics';
import {
NxJson,
readJsonInTree,
readWorkspace,
updateJsonInTree,
updateNxJsonInTree,
updateWorkspaceInTree,
} from '@nrwl/workspace';
import { SchematicOptions } from './schema';
type TsConfigJson = { compilerOptions: { paths: Record<string, string[]> } };
type TsLintJson = { rules: Record<string, { options: string[] }> };
/* --- Helper --- */
function getLibGenerator(type: SchematicOptions['type']): '@nrwl/workspace' | '@nrwl/angular' {
switch (type) {
case 'feature':
case 'data-access':
case 'ui':
return '@nrwl/angular';
case 'util':
case 'models':
return '@nrwl/workspace';
default:
const fallThroughGuard: never = type;
throw new Error(`Unexpected library type: ${type}`);
}
}
function getImportAlias(tree: Tree, projectName: string): { main: string; testing: string } {
const { npmScope } = readJsonInTree<NxJson>(tree, 'nx.json');
const workspace: experimental.workspace.WorkspaceSchema = readWorkspace(tree);
const project = workspace.projects[projectName];
const projectDirectory = project.root.slice('libs/'.length);
return {
main: `@${npmScope}/${projectDirectory}`,
testing: `@${npmScope}/${projectDirectory}/testing`,
};
}
/* --- Rules --- */
function configureTestTarget(projectName: string): Rule {
return updateWorkspaceInTree((workspace: experimental.workspace.WorkspaceSchema) => {
const project = workspace.projects[projectName];
if (!project.architect || !project.architect.test) {
return workspace;
}
if (!project.architect.test.configurations) {
project.architect.test.configurations = {};
}
project.architect.test.outputs = [`coverage/${project.root}`];
project.architect.test.configurations.coverage = {
codeCoverage: true,
};
return workspace;
});
}
function configureLintTarget(projectName: string): Rule {
return updateWorkspaceInTree((workspace: experimental.workspace.WorkspaceSchema) => {
const project = workspace.projects[projectName];
if (!project.architect || !project.architect.lint) {
return workspace;
}
if (!project.architect.lint.configurations) {
project.architect.lint.configurations = {};
}
project.architect.lint.configurations.fix = {
fix: true,
};
return workspace;
});
}
function removeSchematicDefaults(projectName: string): Rule {
return updateWorkspaceInTree((workspace: experimental.workspace.WorkspaceSchema) => {
const project = workspace.projects[projectName];
if (!project.schematics) {
return workspace;
}
project.schematics = {};
return workspace;
});
}
function addTestingBarrel(projectName: string): Rule {
return tree => {
const workspace: experimental.workspace.WorkspaceSchema = readWorkspace(tree);
const project = workspace.projects[projectName];
const testingBarrelPath = `${project.sourceRoot}/testing.ts`;
tree.create(testingBarrelPath, '');
return updateJsonInTree<TsConfigJson>('tsconfig.json', tsConfig => {
const compilerOptions = tsConfig.compilerOptions;
const alias = getImportAlias(tree, projectName);
compilerOptions.paths[alias.testing] = [testingBarrelPath];
return tsConfig;
});
};
}
function configureImportBlacklist(projectName: string): Rule {
return tree => {
const workspace: experimental.workspace.WorkspaceSchema = readWorkspace(tree);
const project = workspace.projects[projectName];
const tsLintRoot = readJsonInTree<TsLintJson>(tree, 'tslint.json');
const blacklistRoot = tsLintRoot.rules['import-blacklist'].options;
const importAlias = getImportAlias(tree, projectName);
const blacklistProject = [importAlias.main, importAlias.testing];
return updateJsonInTree<TsLintJson>(`${project.root}/tslint.json`, tsLint => {
if (!tsLint.rules) {
tsLint.rules = {};
}
tsLint.rules['import-blacklist'] = { options: [...blacklistRoot, ...blacklistProject] };
return tsLint;
});
};
}
function sortTsConfigPaths(): Rule {
return updateJsonInTree<TsConfigJson>('tsconfig.json', tsConfig => {
const pathsSortedByAlias = Object.entries(tsConfig.compilerOptions.paths)
.sort(([aliasA], [aliasB]) => {
const _aliasA = aliasA.replace('/testing', '');
const _aliasB = aliasB.replace('/testing', '');
if (_aliasA === _aliasB) {
return aliasA < aliasB ? -1 : 1;
}
return _aliasA < _aliasB ? -1 : 1;
})
.reduce((paths, [alias, path]) => ({ ...paths, [alias]: path }), {});
tsConfig.compilerOptions.paths = pathsSortedByAlias;
return tsConfig;
});
}
function sortProjects(): Rule {
const sortProjectsByName = (projects: Record<string, any>) =>
Object.entries(projects)
.sort(([nameA], [nameB]) => {
return nameA < nameB ? -1 : 1;
})
.reduce((sortedProjects, [name, config]) => ({ ...sortedProjects, [name]: config }), {});
return chain([
updateWorkspaceInTree((workspace: experimental.workspace.WorkspaceSchema) => {
workspace.projects = sortProjectsByName(workspace.projects);
return workspace;
}),
updateNxJsonInTree(nxJson => {
nxJson.projects = sortProjectsByName(nxJson.projects);
return nxJson;
}),
]);
}
/* --- Schematic --- */
const defaultLibOptions = {
simpleModuleName: true,
style: 'scss',
unitTestRunner: 'jest',
};
export default function (schema: SchematicOptions): Rule {
const libName = `${schema.type}-${schema.name}`;
const projectName = `${schema.scope}/${libName}`.replace(new RegExp('/', 'g'), '-');
const libGenerator = getLibGenerator(schema.type);
const libOptions = {
name: libName,
directory: schema.scope,
tags: `scope:${schema.scope},type:${schema.type}`,
...defaultLibOptions,
};
return chain([
externalSchematic(libGenerator, 'lib', libOptions),
removeSchematicDefaults(projectName),
configureLintTarget(projectName),
configureTestTarget(projectName),
addTestingBarrel(projectName),
configureImportBlacklist(projectName),
sortTsConfigPaths(),
sortProjects(),
]);
}
export interface SchematicOptions {
name: string;
type: 'feature' | 'data-access' | 'ui' | 'util' | 'models';
scope: 'shared' | '<add other scopes here>';
}
{
"$schema": "http://json-schema.org/draft-07/schema",
"id": "lib",
"type": "object",
"properties": {
"name": {
"description": "The name of the library.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "How would you like to call the library? (without any prefix)"
},
"type": {
"description": "The type/purpose of the library (\"feature\", \"data-access\", \"ui\", \"util\", \"models\").",
"type": "string",
"enum": ["feature", "data-access", "ui", "util", "models"],
"x-prompt": {
"message": "What type of library do you want to generate?",
"type": "list",
"items": [
{
"value": "feature",
"label": "feature library - routed pages, smart/container components, connection between ui & data-access"
},
{
"value": "data-access",
"label": "data-access library - NgRx, api services for backend communication"
},
{
"value": "ui",
"label": "ui library - pure dumb/presentational components"
},
{
"value": "util",
"label": "util library - utility functions, utility types, small helpers, ..."
},
{
"value": "models",
"label": "models library - interfaces, types, ..."
}
]
}
},
"scope": {
"description": "The library's scope/domain (\"shared\", \"<add other scopes here>\").",
"type": "string",
"enum": ["shared", "<add other scopes here>"],
"x-prompt": "Which scope/domain does your library belong to?"
}
},
"required": ["name", "type", "scope"]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment