Last active
June 10, 2020 18:21
-
-
Save twittwer/d1adb85f873b02b14b6f52d2aa9ec4f3 to your computer and use it in GitHub Desktop.
Workspace Schematic For Nx Libraries
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | |
]); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface SchematicOptions { | |
name: string; | |
type: 'feature' | 'data-access' | 'ui' | 'util' | 'models'; | |
scope: 'shared' | '<add other scopes here>'; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"$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