ng-alain
Version:
Schematics specific to ng-alain
291 lines (259 loc) • 9.97 kB
text/typescript
import { strings, normalize } from '@angular-devkit/core';
import { ProjectDefinition } from '@angular-devkit/core/src/workspace';
import {
apply,
applyTemplates,
branchAndMerge,
chain,
filter,
mergeWith,
move,
noop,
Rule,
SchematicsException,
Tree,
url
} from '@angular-devkit/schematics';
import { Schema as ComponentSchema } from '@schematics/angular/component/schema';
import { insertImport, getSourceNodes } from '@schematics/angular/utility/ast-utils';
import { InsertChange } from '@schematics/angular/utility/change';
import { buildRelativePath, findModuleFromOptions, ModuleOptions } from '@schematics/angular/utility/find-module';
import { parseName } from '@schematics/angular/utility/parse-name';
import { validateHtmlSelector } from '@schematics/angular/utility/validation';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import { addServiceToModuleOrStandalone, findRoutesPath, getSourceFile, ROUTINS_FILENAME } from './ast';
import { isStandalone } from './standalone';
import { getProject, NgAlainProjectDefinition } from './workspace';
export interface CommonSchema extends ComponentSchema {
[key: string]: any;
_filesPath?: string;
schematicName?: string;
target?: string;
componentName?: string;
serviceName?: string;
importModulePath?: string;
routerModulePath?: string;
selector?: string;
withoutPrefix?: boolean;
withoutModulePrefixInComponentName?: boolean;
skipTests?: boolean;
service?: 'ignore' | 'none' | 'root';
flat?: boolean;
modal?: boolean;
}
function buildSelector(schema: CommonSchema, projectPrefix: string): string {
const ret: string[] = [];
if (!schema.withoutPrefix) {
if (schema.prefix) {
ret.push(schema.prefix);
} else if (schema.prefix === undefined && projectPrefix) {
ret.push(projectPrefix);
}
}
// module name
if (schema.module) {
ret.push(schema.module);
}
// target name
if (schema.target) {
ret.push(...schema.target.split('/'));
}
// name
ret.push(strings.dasherize(schema.name!));
return ret.join('-');
}
function buildName(schema: CommonSchema, prefix: 'Component' | 'Service'): string {
const ret: string[] = schema.withoutModulePrefixInComponentName === true ? [] : [schema.module!];
if (schema.target && schema.target.length > 0) {
ret.push(...schema.target.split('/'));
}
ret.push(schema.name!);
// 服务类自动过滤 list, empty 两个页面的后缀
if (prefix === 'Service' && ['list', 'empty'].includes(schema.name)) {
ret.pop();
}
ret.push(prefix);
return strings.classify(ret.join('-'));
}
export function refreshPathRoot(
project: ProjectDefinition,
schema: CommonSchema,
alainProject: NgAlainProjectDefinition
): void {
if (schema.path === undefined) {
schema.path = `/${path.join(project.sourceRoot!, alainProject?.routesRoot ?? 'app/routes')}`;
}
}
function resolveSchema(
tree: Tree,
project: ProjectDefinition,
schema: CommonSchema,
alainProject: NgAlainProjectDefinition
): void {
// module name
if (!schema.module) {
throw new SchematicsException(`Must specify module name. (e.g: ng g ng-alain:list <list name> -m=<module name>)`);
}
// path
refreshPathRoot(project, schema, alainProject);
schema.path += `/${schema.module}`;
const parsedPath = parseName(schema.path, schema.name!);
schema.name = parsedPath.name;
schema.path = parsedPath.path;
const rootPath = path.resolve(__dirname, '../../..');
const fullPath = path.join(rootPath, schema.path, schema.name);
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length > 0) {
throw new SchematicsException(`The directory (${fullPath}) already exists`);
}
if (schema.standalone) {
schema.importModulePath = normalize(`${schema.path}/${schema.name}/${schema.name}.component.ts`);
} else {
schema.importModulePath = findModuleFromOptions(tree, schema as unknown as ModuleOptions);
}
if (!schema._filesPath) {
// 若基础页尝试从 `_cli-tpl/_${schema.schematicName!}` 下查找该目录,若存在则优先使用
if (['list', 'edit', 'view', 'empty'].includes(schema.schematicName!)) {
const overrideDir = `/${[project.root, `_cli-tpl/_${schema.schematicName}`].filter(i => !!i).join('/')}`;
const overridePath = `${overrideDir}/__path__/__name@dasherize@if-flat__/__name@dasherize__.component.ts`;
if (tree.exists(overridePath) || tree.exists(`${overridePath}.template`)) {
// 所在目录与命令目录同属一个目录结构,因此无须特殊处理
schema._filesPath = path.relative(__dirname, rootPath) + overrideDir;
}
}
schema._filesPath = schema._filesPath || './files';
}
// fill target
if (schema.target) {
schema.path += strings.dasherize(`/${schema.target}`);
}
if (schema.standalone) {
schema.routerModulePath = findRoutesPath(tree, schema.path);
if (schema.routerModulePath.length <= 0) {
throw new SchematicsException(`Could not find a non Routing file: ${ROUTINS_FILENAME}`);
}
} else {
schema.routerModulePath = schema.importModulePath!.replace('.module.ts', '-routing.module.ts');
}
// html selector
schema.selector = schema.selector || buildSelector(schema, project.prefix);
validateHtmlSelector(schema.selector);
}
export function addImportToModule(tree: Tree, filePath: string, symbolName: string, fileName: string): void {
const source = getSourceFile(tree, filePath);
const change = insertImport(source, filePath, symbolName, fileName) as InsertChange;
if (change.path == null) return;
const declarationRecorder = tree.beginUpdate(filePath);
declarationRecorder.insertLeft(change.pos, change.toAdd);
tree.commitUpdate(declarationRecorder);
}
export function addValueToVariable(
tree: Tree,
filePath: string,
variableName: string,
text: string,
needWrap: boolean = true
): void {
const source = getSourceFile(tree, filePath);
const node = getSourceNodes(source).find(
node => node.kind == ts.SyntaxKind.Identifier && node.getText() === variableName
);
if (!node) {
throw new SchematicsException(`Could not find any [${variableName}] variable in path '${filePath}'.`);
}
const arr = (node.parent as any).initializer as ts.ArrayLiteralExpression;
const change = new InsertChange(
filePath,
arr.end - 1,
`${arr.elements && arr.elements.length > 0 ? ',' : ''}${needWrap ? '\n ' : ''}${text}`
);
const declarationRecorder = tree.beginUpdate(filePath);
declarationRecorder.insertLeft(change.pos, change.toAdd);
tree.commitUpdate(declarationRecorder);
}
function getRelativePath(filePath: string, schema: CommonSchema, prefix: 'component' | 'service'): string {
const importPath = normalize(
`/${schema.path}/${schema.flat ? '' : `${strings.dasherize(schema.name!)}/`}${strings.dasherize(
schema.name!
)}.${prefix}`
);
return buildRelativePath(filePath, importPath);
}
function addDeclaration(schema: CommonSchema): Rule {
return (tree: Tree) => {
if (schema.skipImport || !schema.module) {
return tree;
}
// imports
if (!schema.standalone) {
addImportToModule(
tree,
schema.importModulePath!,
schema.componentName!,
getRelativePath(schema.importModulePath!, schema, 'component')
);
addValueToVariable(tree, schema.importModulePath!, 'COMPONENTS', schema.componentName!);
}
// component
if (schema.modal !== true) {
// routing
addImportToModule(
tree,
schema.routerModulePath!,
schema.componentName!,
getRelativePath(schema.routerModulePath!, schema, 'component')
);
addValueToVariable(
tree,
schema.routerModulePath!,
'routes',
`{ path: '${schema.name}', component: ${schema.componentName} }`
);
}
// service
if (schema.service === 'none') {
addServiceToModuleOrStandalone(
tree,
schema.standalone,
schema.importModulePath!,
schema.serviceName!,
getRelativePath(schema.importModulePath!, schema, 'service')
);
}
return tree;
};
}
export function buildAlain(schema: CommonSchema): Rule {
return async (tree: Tree) => {
const res = await getProject(tree, schema.project);
if (schema.project && res.name !== schema.project) {
throw new SchematicsException(`The specified project does not match '${schema.project}', current: ${res.name}`);
}
const project = res.project;
// standalone
schema.standalone = await isStandalone(tree, schema.standalone, res.name);
resolveSchema(tree, project, schema, res.alainProject);
schema.componentName = buildName(schema, 'Component');
schema.serviceName = buildName(schema, 'Service');
// Don't support inline
schema.inlineTemplate = false;
const templateSource = apply(url(schema._filesPath!), [
filter(filePath => !filePath.endsWith('.DS_Store')),
schema.service === 'ignore' ? filter(filePath => !filePath.endsWith('.service.ts.template')) : noop(),
schema.skipTests ? filter(filePath => !filePath.endsWith('.spec.ts.template')) : noop(),
schema.inlineStyle ? filter(filePath => !filePath.endsWith('.__style__.template')) : noop(),
schema.inlineTemplate ? filter(filePath => !filePath.endsWith('.html.template')) : noop(),
// schema.spec ? noop() : filter(filePath => !filePath.endsWith('.spec.ts')),
// schema.inlineStyle ? filter(filePath => !filePath.endsWith('.__styleext__')) : noop(),
// schema.inlineTemplate ? filter(filePath => !filePath.endsWith('.html')) : noop(),
applyTemplates({
...strings,
'if-flat': (s: string) => (schema.flat ? '' : s),
...schema
}),
move(null!, `${schema.path}/`)
]);
return chain([branchAndMerge(chain([mergeWith(templateSource), addDeclaration(schema)]))]);
};
}