diff --git a/packages/schematics/application/files/src/app/core/startup/startup.service.ts b/packages/schematics/application/files/src/app/core/startup/startup.service.ts index 561a2510f..12162786d 100644 --- a/packages/schematics/application/files/src/app/core/startup/startup.service.ts +++ b/packages/schematics/application/files/src/app/core/startup/startup.service.ts @@ -9,15 +9,9 @@ import { ACLService } from '@delon/acl';<% if (i18n) { %> import { TranslateService } from '@ngx-translate/core'; import { I18NService } from '../i18n/i18n.service';<% } %> -// #region static loading icons -// @see http://ng.ant.design/components/icon/zh#%E9%9D%99%E6%80%81%E5%8A%A0%E8%BD%BD%E4%B8%8E%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD - import { NzIconService } from 'ng-zorro-antd'; -import {} from '@ant-design/icons-angular/icons'; - -const ICONS = []; - -// #endregion +import { ICONS_AUTO } from '../../../style-icons-auto'; +import { ICONS } from '../../../style-icons'; /** * 用于应用启动时 @@ -37,7 +31,7 @@ export class StartupService { private httpClient: HttpClient, private injector: Injector ) { - iconSrv.addIcon(...ICONS); + iconSrv.addIcon(...ICONS_AUTO, ...ICONS); } private viaHttp(resolve: any, reject: any) { diff --git a/packages/schematics/curd/schema.d.ts b/packages/schematics/curd/schema.d.ts deleted file mode 100644 index c887c6782..000000000 --- a/packages/schematics/curd/schema.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface Schema { - /** - * The path to create the component. - */ - path?: string; - /** - * The name of the project. - */ - project?: string; - /** - * The name of the component. - */ - name: string; - /** - * Specifies if the style will be in the ts file. - */ - inlineStyle?: boolean; - /** - * Specifies if the template will be in the ts file. - */ - inlineTemplate?: boolean; - /** - * Specifies the view encapsulation strategy. - */ - viewEncapsulation?: 'Emulated' | 'Native' | 'None'; - /** - * Specifies the change detection strategy. - */ - changeDetection?: 'Default' | 'OnPush'; - /** - * The prefix to apply to generated selectors. - */ - prefix?: string; - /** - * The file extension to be used for style files. - */ - styleext?: string; - /** - * Specifies if a spec file is generated. - */ - spec?: boolean; - /** - * Flag to indicate if a dir is created. - */ - flat?: boolean; - /** - * Flag to skip the module import. - */ - skipImport?: boolean; - /** - * The selector to use for the component. - */ - selector?: string; - /** - * Allows specification of the declaring module. - */ - module?: string; - /** - * Specifies if declaring module exports the component. - */ - export?: boolean; - /** - * Specifies using modal mode. - */ - modal?: boolean; - /** - * Without prefix to selectors - */ - withoutPrefix?: boolean; -} diff --git a/packages/schematics/curd/schema.json b/packages/schematics/curd/schema.json index ce6179b63..5960912b6 100644 --- a/packages/schematics/curd/schema.json +++ b/packages/schematics/curd/schema.json @@ -89,25 +89,40 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, "export": { "type": "boolean", "default": false, "description": "Specifies if declaring module exports the component." }, + "entryComponent": { + "type": "boolean", + "default": false, + "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module. (e.g., -m=trade)", + "alias": "m" + }, + "target": { + "type": "string", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", + "alias": "t" + }, "withoutPrefix": { "type": "boolean", - "description": "Without prefix to selectors", + "description": "组件名不加前缀 (Without prefix to selectors)", "default": false }, "modal": { "type": "boolean", "default": true, - "description": "Specifies using modal mode." + "description": "指定是否使用模态框 (Specifies using modal mode)" } }, "required": [] diff --git a/packages/schematics/curd/schema.ts b/packages/schematics/curd/schema.ts new file mode 100644 index 000000000..fdcdceda9 --- /dev/null +++ b/packages/schematics/curd/schema.ts @@ -0,0 +1,16 @@ +import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; + +export interface Schema extends ComponentSchema { + /** + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) + */ + target?: string; + /** + * 指定组件名不加前缀 (Without prefix to selectors) + */ + withoutPrefix?: boolean; + /** + * 指定是否使用模态框 (Specifies using modal mode) + */ + modal?: boolean; +} diff --git a/packages/schematics/docs/generate.en-US.md b/packages/schematics/docs/generate.en-US.md index 80ecc7330..eda65f6bd 100644 --- a/packages/schematics/docs/generate.en-US.md +++ b/packages/schematics/docs/generate.en-US.md @@ -63,7 +63,7 @@ sys sys.module.ts ``` -So when you want to generate a view page that should be under the `log` directory: +So when you want to generate a view page that should be under the `log` directory (could be set like `log/list`): ```bash ng g ng-alain:view view -m=sys -t=log @@ -99,7 +99,7 @@ For example, to create a custom edit page template, you only need to create the After that, just run: ```bash -ng g ng-alain:tpl -m=trade +ng g ng-alain:tpl [your template name] [name] -m=trade ``` ### How to write a template file diff --git a/packages/schematics/docs/generate.zh-CN.md b/packages/schematics/docs/generate.zh-CN.md index ab02390e6..a2c8ffc6c 100644 --- a/packages/schematics/docs/generate.zh-CN.md +++ b/packages/schematics/docs/generate.zh-CN.md @@ -62,7 +62,7 @@ sys sys.module.ts ``` -因此,当你希望生成的查看应该是在 `log` 组件下面时,你可以这样子: +因此,当你希望生成的查看应该是在 `log` 组件(支持 `log/list` 多级写法)下面时,你可以这样子: ```bash ng g ng-alain:view view -m=sys -t=log @@ -96,7 +96,7 @@ ng g ng-alain:edit --modal=false 之后,只需要运行: ```bash -ng g ng-alain:tpl -m=trade +ng g ng-alain:tpl [your template name] [name] -m=trade ``` > 自定义页参数同业务页一致。 diff --git a/packages/schematics/docs/getting-started.en-US.md b/packages/schematics/docs/getting-started.en-US.md index c87cb2cff..1e016a1dc 100644 --- a/packages/schematics/docs/getting-started.en-US.md +++ b/packages/schematics/docs/getting-started.en-US.md @@ -14,8 +14,6 @@ Using ng-alain scaffolding should as use as possible the `ng` command set provid ## Installation -我们不建议直接克隆 Github 源代码,而应该使用 `ng add` 来构建 ng-alain 项目,而构建一个空 ng-alain 只需要简单几个动作: - We don't recommend directly cloning the git repository, but instead using `ng add` to build the ng-alain project, there are a few simple steps: 1. Create an empty angular project diff --git a/packages/schematics/docs/plugin.en-US.md b/packages/schematics/docs/plugin.en-US.md index 71c377259..e62b874e0 100644 --- a/packages/schematics/docs/plugin.en-US.md +++ b/packages/schematics/docs/plugin.en-US.md @@ -116,3 +116,29 @@ ng g ng-alain:plugin networkEnv -packageManager=npm -t=remove # remove yarn ng g ng-alain:plugin networkEnv -packageManager=yarn -t=remove ``` + +### icon + +From the project to analyze and generate static load Icon, The plugin will automatically generate two files in the `src` directory: + +- `src/style-icons.ts` Custom Icon (e.g: remote menu icon) +- `src/style-icons-auto.ts` command automatically generates files + +```bash +ng g ng-alain:plugin icon +``` + +Also, you need to manually import in `startup.service.ts`: + +```ts +import { ICONS_AUTO } from '../../../style-icons-auto'; +import { ICONS } from '../../../style-icons'; + +@Injectable() +export class StartupService { + constructor(iconSrv: NzIconService) { + iconSrv.addIcon(...ICONS_AUTO, ...ICONS); + } +} +``` + diff --git a/packages/schematics/docs/plugin.zh-CN.md b/packages/schematics/docs/plugin.zh-CN.md index b17a6bd8a..98aa10f09 100644 --- a/packages/schematics/docs/plugin.zh-CN.md +++ b/packages/schematics/docs/plugin.zh-CN.md @@ -120,3 +120,28 @@ ng g ng-alain:plugin networkEnv -packageManager=npm -t=remove # remove yarn ng g ng-alain:plugin networkEnv -packageManager=yarn -t=remove ``` + +### icon + +**尽可能**从项目中分析并生成静态 Icon,插件会自动在 `src` 目录下生成两个文件: + +- `src/style-icons.ts` 自定义部分无法解析(例如:远程菜单图标) +- `src/style-icons-auto.ts` 命令自动生成文件 + +```bash +ng g ng-alain:plugin icon +``` + +同时,需要手动在 `startup.service.ts` 中导入: + +```ts +import { ICONS_AUTO } from '../../../style-icons-auto'; +import { ICONS } from '../../../style-icons'; + +@Injectable() +export class StartupService { + constructor(iconSrv: NzIconService) { + iconSrv.addIcon(...ICONS_AUTO, ...ICONS); + } +} +``` diff --git a/packages/schematics/edit/schema.json b/packages/schematics/edit/schema.json index 5627de1ff..1bafef760 100644 --- a/packages/schematics/edit/schema.json +++ b/packages/schematics/edit/schema.json @@ -34,7 +34,8 @@ "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", - "default": false + "default": false, + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", @@ -88,30 +89,40 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, "export": { "type": "boolean", "default": false, "description": "Specifies if declaring module exports the component." }, - "withoutPrefix": { + "entryComponent": { "type": "boolean", - "description": "Without prefix to selectors", - "default": false + "default": false, + "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module. (e.g., -m=trade)", + "alias": "m" }, "target": { "type": "string", - "description": "Specifies relative path.", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", "alias": "t" }, + "withoutPrefix": { + "type": "boolean", + "description": "组件名不加前缀 (Without prefix to selectors)", + "default": false + }, "modal": { "type": "boolean", "default": true, - "description": "Specifies using modal mode." + "description": "指定是否使用模态框 (Specifies using modal mode)" } }, "required": [] diff --git a/packages/schematics/edit/schema.ts b/packages/schematics/edit/schema.ts index 6dac25d37..fdcdceda9 100644 --- a/packages/schematics/edit/schema.ts +++ b/packages/schematics/edit/schema.ts @@ -2,15 +2,15 @@ import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; export interface Schema extends ComponentSchema { /** - * Without prefix to selectors + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) */ - withoutPrefix?: boolean; + target?: string; /** - * Specifies relative path. + * 指定组件名不加前缀 (Without prefix to selectors) */ - target?: string; + withoutPrefix?: boolean; /** - * Specifies using modal mode. + * 指定是否使用模态框 (Specifies using modal mode) */ modal?: boolean; } diff --git a/packages/schematics/empty/schema.d.ts b/packages/schematics/empty/schema.d.ts deleted file mode 100644 index 71663ad1f..000000000 --- a/packages/schematics/empty/schema.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface Schema { - /** - * The path to create the component. - */ - path?: string; - /** - * The name of the project. - */ - project?: string; - /** - * The name of the component. - */ - name: string; - /** - * Specifies if the style will be in the ts file. - */ - inlineStyle?: boolean; - /** - * Specifies if the template will be in the ts file. - */ - inlineTemplate?: boolean; - /** - * Specifies the view encapsulation strategy. - */ - viewEncapsulation?: 'Emulated' | 'Native' | 'None'; - /** - * Specifies the change detection strategy. - */ - changeDetection?: 'Default' | 'OnPush'; - /** - * The prefix to apply to generated selectors. - */ - prefix?: string; - /** - * The file extension to be used for style files. - */ - styleext?: string; - /** - * Specifies if a spec file is generated. - */ - spec?: boolean; - /** - * Flag to indicate if a dir is created. - */ - flat?: boolean; - /** - * Flag to skip the module import. - */ - skipImport?: boolean; - /** - * The selector to use for the component. - */ - selector?: string; - /** - * Allows specification of the declaring module. - */ - module?: string; - /** - * Specifies if declaring module exports the component. - */ - export?: boolean; - /** - * Specifies relative path. - */ - target?: string; - /** - * Without prefix to selectors - */ - withoutPrefix?: boolean; -} diff --git a/packages/schematics/empty/schema.json b/packages/schematics/empty/schema.json index 465eaa46a..05b50a6aa 100644 --- a/packages/schematics/empty/schema.json +++ b/packages/schematics/empty/schema.json @@ -34,7 +34,8 @@ "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", - "default": false + "default": false, + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", @@ -88,24 +89,34 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module. (e.g., -m=trade)", - "alias": "m" - }, "export": { "type": "boolean", "default": false, "description": "Specifies if declaring module exports the component." }, + "entryComponent": { + "type": "boolean", + "default": false, + "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module. (e.g., -m=trade)", + "alias": "m" + }, "target": { "type": "string", - "description": "Specifies relative path.", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", "alias": "t" }, "withoutPrefix": { "type": "boolean", - "description": "Without prefix to selectors", + "description": "组件名不加前缀 (Without prefix to selectors)", "default": false } }, diff --git a/packages/schematics/empty/schema.ts b/packages/schematics/empty/schema.ts new file mode 100644 index 000000000..b4493a431 --- /dev/null +++ b/packages/schematics/empty/schema.ts @@ -0,0 +1,12 @@ +import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; + +export interface Schema extends ComponentSchema { + /** + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) + */ + target?: string; + /** + * 指定组件名不加前缀 (Without prefix to selectors) + */ + withoutPrefix?: boolean; +} diff --git a/packages/schematics/list/index.spec.ts b/packages/schematics/list/index.spec.ts index 85b5ed2c5..850503789 100644 --- a/packages/schematics/list/index.spec.ts +++ b/packages/schematics/list/index.spec.ts @@ -35,4 +35,9 @@ describe('Schematic: list', () => { it('shuold be exclude style', () => { expect(tree.readContent(tsPath)).not.toContain(`styleUrls`); }); + + it('should be support targets (like: list/edit)', () => { + tree = runner.runSchematic('list', { name: 'list2', module: 'trade', target: 'list/edit' }, tree); + expect(tree.exists(`/foo/src/app/routes/trade/list/edit/list2/list2.component.html`)).toBe(true); + }); }); diff --git a/packages/schematics/list/schema.d.ts b/packages/schematics/list/schema.d.ts deleted file mode 100644 index 71663ad1f..000000000 --- a/packages/schematics/list/schema.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface Schema { - /** - * The path to create the component. - */ - path?: string; - /** - * The name of the project. - */ - project?: string; - /** - * The name of the component. - */ - name: string; - /** - * Specifies if the style will be in the ts file. - */ - inlineStyle?: boolean; - /** - * Specifies if the template will be in the ts file. - */ - inlineTemplate?: boolean; - /** - * Specifies the view encapsulation strategy. - */ - viewEncapsulation?: 'Emulated' | 'Native' | 'None'; - /** - * Specifies the change detection strategy. - */ - changeDetection?: 'Default' | 'OnPush'; - /** - * The prefix to apply to generated selectors. - */ - prefix?: string; - /** - * The file extension to be used for style files. - */ - styleext?: string; - /** - * Specifies if a spec file is generated. - */ - spec?: boolean; - /** - * Flag to indicate if a dir is created. - */ - flat?: boolean; - /** - * Flag to skip the module import. - */ - skipImport?: boolean; - /** - * The selector to use for the component. - */ - selector?: string; - /** - * Allows specification of the declaring module. - */ - module?: string; - /** - * Specifies if declaring module exports the component. - */ - export?: boolean; - /** - * Specifies relative path. - */ - target?: string; - /** - * Without prefix to selectors - */ - withoutPrefix?: boolean; -} diff --git a/packages/schematics/list/schema.json b/packages/schematics/list/schema.json index 8825e3b8c..7b71ee11b 100644 --- a/packages/schematics/list/schema.json +++ b/packages/schematics/list/schema.json @@ -34,7 +34,8 @@ "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", - "default": false + "default": false, + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", @@ -88,24 +89,34 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module. (e.g., -m=trade)", - "alias": "m" - }, "export": { "type": "boolean", "default": false, "description": "Specifies if declaring module exports the component." }, + "entryComponent": { + "type": "boolean", + "default": false, + "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module. (e.g., -m=trade)", + "alias": "m" + }, "target": { "type": "string", - "description": "Specifies relative path.", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", "alias": "t" }, "withoutPrefix": { "type": "boolean", - "description": "Without prefix to selectors", + "description": "组件名不加前缀 (Without prefix to selectors)", "default": false } }, diff --git a/packages/schematics/list/schema.ts b/packages/schematics/list/schema.ts new file mode 100644 index 000000000..b4493a431 --- /dev/null +++ b/packages/schematics/list/schema.ts @@ -0,0 +1,12 @@ +import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; + +export interface Schema extends ComponentSchema { + /** + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) + */ + target?: string; + /** + * 指定组件名不加前缀 (Without prefix to selectors) + */ + withoutPrefix?: boolean; +} diff --git a/packages/schematics/ng-update/upgrade-rules/v2/v2DomRule.ts b/packages/schematics/ng-update/upgrade-rules/v2/v2DomRule.ts index c0108c6ce..0361ca004 100644 --- a/packages/schematics/ng-update/upgrade-rules/v2/v2DomRule.ts +++ b/packages/schematics/ng-update/upgrade-rules/v2/v2DomRule.ts @@ -251,6 +251,8 @@ function fixTs(host: Tree, path: string) { export function v2DomRule(): Rule { return (host: Tree, context: SchematicContext) => { host.visit(path => { + if (~path.indexOf(`/node_modules/`)) return ; + if (path.endsWith('.ts')) { fixTs(host, path); } diff --git a/packages/schematics/plugin/index.ts b/packages/schematics/plugin/index.ts index 211be81ad..340a9775f 100644 --- a/packages/schematics/plugin/index.ts +++ b/packages/schematics/plugin/index.ts @@ -18,6 +18,7 @@ import { pluginNetworkEnv } from './plugin.network-env'; import { pluginHmr } from './plugin.hmr'; import { pluginDocker } from './plugin.docker'; import { pluginAsdf } from './plugin.asdf'; +import { pluginIcon } from './plugin.icon'; function installPackages() { return (host: Tree, context: SchematicContext) => { @@ -69,6 +70,9 @@ export default function(options: PluginSchema): Rule { ), ); break; + case 'icon': + rules.push(pluginIcon(pluginOptions)); + break; case 'asdf': rules.push(pluginAsdf(pluginOptions)); break; diff --git a/packages/schematics/plugin/plugin.icon.spec.ts b/packages/schematics/plugin/plugin.icon.spec.ts new file mode 100644 index 000000000..dd5b0a791 --- /dev/null +++ b/packages/schematics/plugin/plugin.icon.spec.ts @@ -0,0 +1,78 @@ +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import { createAlainApp } from '../utils/testing'; + +const testCases = { + 'style-icons.ts': ` + import { FilterOutline, StepBackwardFill } from '@ant-design/icons-angular/icons'; + export const ICONS = [ FilterOutline, StepBackwardFill ]; + `, + 'test-icon.ts': ` + import { Component } from '@angular/core'; + @Component({ + selector: 'test-comp', + template: \` + + + + + + + + + + + + + + + + + \` + }) + export class TestComponent {} + `, +}; + +describe('NgAlainSchematic: plugin: icon', () => { + let runner: SchematicTestRunner; + let tree: UnitTestTree; + + beforeEach(() => { + ({ runner, tree } = createAlainApp()); + Object.keys(testCases).forEach(name => + tree.create(`/foo/src/${name}`, testCases[name]), + ); + tree = runner.runSchematic('plugin', { name: 'icon', type: 'add' }, tree); + }); + + it(`should working`, () => { + const path = `/foo/src/style-icons-auto.ts`; + expect(tree.exists(path)).toBe(true); + const content = tree.readContent(path); + // ingore custom icons + expect(content).not.toContain(`FilterOutline`); + expect(content).not.toContain(`StepBackwardFill`); + // white icons + expect(content).not.toContain(`LoadingOutline`); + // type="{{value ? 'icon' : 'icon' }}" + expect(content).toContain(`ArrowLeftOutline`); + expect(content).toContain(`ArrowRightOutline`); + // type="align-{{type ? 'left' : 'right'}}" + expect(content).toContain(`AlignLeftOutline`); + expect(content).toContain(`AlignRightOutline`); + // [type]="value ? 'icon' : 'icon'" + expect(content).toContain(`FullscreenExitOutline`); + expect(content).toContain(`FullscreenOutline`); + // condition: type & theme + expect(content).toContain(`MenuFoldFill`); + expect(content).toContain(`MenuFoldOutline`); + expect(content).toContain(`MenuUnfoldFill`); + expect(content).toContain(`MenuUnfoldOutline`); + // attributes + expect(content).toContain(`ArrowDownOutline`); + expect(content).toContain(`SearchOutline`); + }); +}); diff --git a/packages/schematics/plugin/plugin.icon.ts b/packages/schematics/plugin/plugin.icon.ts new file mode 100644 index 000000000..f46a5ef36 --- /dev/null +++ b/packages/schematics/plugin/plugin.icon.ts @@ -0,0 +1,301 @@ +import { Tree, SchematicContext, Rule } from '@angular-devkit/schematics'; +import { strings } from '@angular-devkit/core'; +import * as ts from 'typescript'; +import { + DefaultTreeDocument, + DefaultTreeElement, + parseFragment, + Attribute, +} from 'parse5'; + +import { PluginOptions } from './interface'; +import { updateComponentMetadata, getSourceFile } from '../utils/ast'; +import { findNodes } from '../utils/devkit-utils/ast-utils'; + +// includes ng-zorro-antd & @delon/* +// - zorro: https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/components/icon/nz-icon.service.ts#L6 +// - @delon: https://github.com/ng-alain/delon/blob/master/packages/theme/src/theme.module.ts#L33 +const WHITE_ICONS = [ + // zorro + 'CalendarOutline', + 'CheckCircleFill', + 'CheckCircleOutline', + 'CheckOutline', + 'ClockCircleOutline', + 'CloseCircleOutline', + 'CloseCircleFill', + 'CloseOutline', + 'DoubleLeftOutline', + 'DoubleRightOutline', + 'DownOutline', + 'ExclamationCircleFill', + 'ExclamationCircleOutline', + 'InfoCircleFill', + 'InfoCircleOutline', + 'LeftOutline', + 'LoadingOutline', + 'PaperClipOutline', + 'QuestionCircleOutline', + 'RightOutline', + 'UploadOutline', + 'UpOutline', + // delon + 'BellOutline', + 'FilterFill', + 'CaretUpOutline', + 'CaretDownOutline', + 'DeleteOutline', + 'PlusOutline', + 'InboxOutline', +]; + +const ATTRIBUTES = { + 'nz-input-group': [ + 'nzAddOnBeforeIcon', + 'nzAddOnAfterIcon', + 'nzPrefixIcon', + 'nzSuffixIcon', + ], + 'nz-avatar': ['nzIcon'], + 'quick-menu': ['icon'], +}; + +const ATTRIBUTE_NAMES = Object.keys(ATTRIBUTES); +// fix parse5 auto ingore lower case all properies +ATTRIBUTE_NAMES.forEach(key => { + const res: string[] = []; + (ATTRIBUTES[key] as string[]).forEach(prop => { + res.push(prop.toLowerCase()); + res.push(`[${prop.toLowerCase()}]`); + }); + ATTRIBUTES[key] = res; +}); + +function findIcons(html: string): string[] { + const res: string[] = []; + const doc = parseFragment(html) as DefaultTreeDocument; + const visitNodes = nodes => { + nodes.forEach(node => { + if (node.attrs) { + const classIcon = genByClass(node); + if (classIcon) res.push(classIcon); + const compIcon = genByComp(node); + if (compIcon) res.push(...compIcon); + const attrIcon = genByAttribute(node); + if (attrIcon) res.push(...attrIcon); + } + + if (node.childNodes) { + visitNodes(node.childNodes); + } + }); + }; + + visitNodes(doc.childNodes); + return res; +} + +function genByClass(node: DefaultTreeElement): string { + const attr = node.attrs.find(a => a.name === 'class'); + if (!attr || !attr.value) return null; + const match = (attr.value as string).match(/anticon(-\w+)+/g); + if (!match || match.length === 0) return null; + return match[0]; +} + +function genByComp(node: DefaultTreeElement): string[] { + if (!node.attrs.find(attr => attr.name === 'nz-icon')) return null; + + const type = node.attrs.find( + attr => attr.name === 'type' || attr.name === '[type]', + ); + if (!type) return null; + + const types = getNgValue(type); + if (types == null) return null; + + const theme = node.attrs.find( + attr => attr.name === 'theme' || attr.name === '[theme]', + ); + const themes = getNgValue(theme); + if (themes == null || themes.length === 0) return types; + + const res = [].concat(...types.map(a => themes.map(b => `${a}#${b}`))); + + return res; +} + +function genByAttribute(node: DefaultTreeElement): string[] { + if (!ATTRIBUTE_NAMES.includes(node.nodeName)) return null; + + const attributes = ATTRIBUTES[node.nodeName]; + const type = node.attrs.find(attr => attributes.includes(attr.name)); + if (!type) return null; + + const types = getNgValue(type); + if (types == null) return null; + + return types; +} + +function getNgValue(attr: Attribute): string[] { + if (!attr) return null; + + const str = attr.value.trim(); + const templatVarIndex = str.indexOf('{{'); + + // type="icon" + // type="{{value ? 'icon' : 'icon' }}" + // type="align-{{value ? 'icon' : 'icon' }}" + if (!attr.name.startsWith('[')) { + const prefix = templatVarIndex > 0 ? str.substr(0, templatVarIndex) : ''; + if (templatVarIndex !== -1) { + return fixValue(str.substr(templatVarIndex), prefix); + } + return [str]; + } + + // ingore {{ }} + if (templatVarIndex !== -1) return null; + + return fixValue(str, ''); +} + +function fixValue(str: string, prefix: string) { + // value ? 'icon' : 'icon' + // focus ? 'anticon anticon-arrow-down' : 'anticon anticon-search' + // 'icon' + const types = + str.replace(/anticon anticon-/g, '').match(/['|"|`][-A-Za-z]+['|"|`]/g) || + []; + if (types.length > 0) { + return types.map(t => prefix + t.replace(/['|"|`]/g, '')); + } + return null; +} + +function fixTs(host: Tree, path: string) { + let res: string[] = []; + updateComponentMetadata( + host, + path, + (node: ts.PropertyAssignment) => { + if (!ts.isStringLiteralLike(node.initializer)) return; + res = findIcons(node.initializer.getText()); + return []; + }, + `template`, + ); + return res; +} + +function getIconNameByClassName(value: string): string { + let res = value.replace(/anticon anticon-/g, '').replace(/anticon-/g, ''); + + if (value === 'anticon-spin' || value.indexOf('-o-') !== -1) { + return null; + } + + if (res.includes('verticle')) { + res = res.replace('verticle', 'vertical'); + } + if (res.startsWith('cross')) { + res = res.replace('cross', 'close'); + } + + if (/(-o)$/.test(res)) { + res = res.replace(/(-o)$/, '-outline'); + } else if (/#outline/.test(res)) { + res = res.replace(/#outline/, '-outline'); + } else if (/#fill/.test(res)) { + res = res.replace(/#fill/, '-fill'); + } else if (/#twotone/.test(res)) { + res = res.replace(/#twotone/, '-TwoTone'); + } else { + res = `${res}-outline`; + } + + return strings.classify(res); +} + +function getIcons(host: Tree): string[] { + const iconClassList: string[] = []; + + host.visit(path => { + if (~path.indexOf(`/node_modules/`)) return; + let res: string[] = []; + if (path.endsWith('.ts')) { + res = fixTs(host, path); + } + if (path.endsWith('.html')) { + res = findIcons(host.read(path).toString()); + } + if (res.length > 0) { + console.log(`found ${JSON.stringify(res)} icons in ${path}\n`); + iconClassList.push(...res); + } + }); + + const iconSet = new Set(); + iconClassList + .map(value => getIconNameByClassName(value)) + .filter(w => w != null && !WHITE_ICONS.includes(w)) + .forEach(v => iconSet.add(v)); + + return Array.from(iconSet).sort(); +} + +function genCustomIcons(options: PluginOptions, host: Tree) { + const path = options.sourceRoot + `/style-icons.ts`; + if (!host.exists(path)) { + host.create( + path, + `// Custom icon static resources + +import { } from '@ant-design/icons-angular/icons'; + +export const ICONS = [ ]; +`, + ); + return; + } + const source = getSourceFile(host, path); + const allImports = findNodes(source, ts.SyntaxKind.ImportDeclaration); + const iconImport = allImports.find((w: ts.ImportDeclaration) => + w.moduleSpecifier.getText().includes('@ant-design/icons-angular/icons'), + ) as ts.ImportDeclaration; + if (!iconImport) return; + (iconImport.importClause.namedBindings as ts.NamedImports)!.elements!.forEach( + v => WHITE_ICONS.push(v.getText().trim()), + ); +} + +function genIconFile(options: PluginOptions, host: Tree, icons: string[]) { + const content = `/* +* Automatically generated by 'ng g ng-alain:plugin icon' +* @see https://ng-alain.com/cli/plugin#icon +*/ + +import { + ${icons.join(',\n ')} +} from '@ant-design/icons-angular/icons'; + +export const ICONS_AUTO = [ + ${icons.join(',\n ')} +]; +`; + const savePath = options.sourceRoot + `/style-icons-auto.ts`; + if (host.exists(savePath)) { + host.overwrite(savePath, content); + } else { + host.create(savePath, content); + } +} + +export function pluginIcon(options: PluginOptions): Rule { + return (host: Tree, context: SchematicContext) => { + genCustomIcons(options, host); + const icons = getIcons(host); + genIconFile(options, host, icons); + }; +} diff --git a/packages/schematics/test.ts b/packages/schematics/test.ts index 31d32545a..2af979191 100644 --- a/packages/schematics/test.ts +++ b/packages/schematics/test.ts @@ -12,7 +12,8 @@ const Jasmine = require('jasmine'); const runner = new Jasmine({ projectBaseDir: projectBaseDir }); // const files = `packages/schematics/**/*.spec.ts`; -const files = `packages/schematics/ng-update/test-cases/v2-test-cases.spec.ts`; +const files = `packages/schematics/plugin/plugin.icon.spec.ts`; +// const files = `packages/schematics/ng-update/test-cases/v2-test-cases.spec.ts`; // const files = `packages/schematics/ng-update/test-cases/misc/deprecated-property-checks.spec.ts`; const tests = glob.sync(files).map(p => relative(projectBaseDir, p)); diff --git a/packages/schematics/tpl/schema.json b/packages/schematics/tpl/schema.json index c8140c8bc..9764f4af9 100644 --- a/packages/schematics/tpl/schema.json +++ b/packages/schematics/tpl/schema.json @@ -19,7 +19,7 @@ }, "tplName": { "type": "string", - "description": "The name of the template directory name.", + "description": "指定模板名称 (Specifies template name)", "$default": { "$source": "argv", "index": 0 @@ -40,8 +40,7 @@ }, "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", - "type": "boolean", - "alias": "t" + "type": "boolean" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", @@ -85,11 +84,6 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, "export": { "type": "boolean", "default": false, @@ -100,20 +94,25 @@ "default": false, "description": "Specifies if the component is an entry component of declaring module." }, - "withoutPrefix": { - "type": "boolean", - "description": "Without prefix to selectors", - "default": false + "module": { + "type": "string", + "description": "Allows specification of the declaring module.", + "alias": "m" }, "target": { "type": "string", - "description": "Specifies relative path.", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", "alias": "t" }, + "withoutPrefix": { + "type": "boolean", + "description": "组件名不加前缀 (Without prefix to selectors)", + "default": false + }, "modal": { "type": "boolean", "default": true, - "description": "Specifies using modal mode." + "description": "指定是否使用模态框 (Specifies using modal mode)" } }, "required": [ ] diff --git a/packages/schematics/tpl/schema.ts b/packages/schematics/tpl/schema.ts index 7adb65638..0186d8f31 100644 --- a/packages/schematics/tpl/schema.ts +++ b/packages/schematics/tpl/schema.ts @@ -2,19 +2,19 @@ import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; export interface Schema extends ComponentSchema { /** - * Specifies template name. + * 指定模板名称 (Specifies template name) */ tplName?: string; /** - * Without prefix to selectors + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) */ - withoutPrefix?: boolean; + target?: string; /** - * Specifies relative path. + * 指定组件名不加前缀 (Without prefix to selectors) */ - target?: string; + withoutPrefix?: boolean; /** - * Specifies using modal mode. + * 指定是否使用模态框 (Specifies using modal mode) */ modal?: boolean; } diff --git a/packages/schematics/utils/alain.ts b/packages/schematics/utils/alain.ts index fca7c689d..d1e637f5d 100644 --- a/packages/schematics/utils/alain.ts +++ b/packages/schematics/utils/alain.ts @@ -56,7 +56,7 @@ function buildSelector(schema: CommonSchema, projectPrefix: string) { } // target name if (schema.target) { - ret.push(schema.target); + ret.push(...schema.target.split('/')); } // name ret.push(strings.dasherize(schema.name)); @@ -65,7 +65,9 @@ function buildSelector(schema: CommonSchema, projectPrefix: string) { function buildComponentName(schema: CommonSchema, projectPrefix: string) { const ret: string[] = [schema.module]; - if (schema.target && schema.target.length > 0) ret.push(schema.target); + if (schema.target && schema.target.length > 0) { + ret.push(...schema.target.split('/')); + } ret.push(schema.name); ret.push(`Component`); return strings.classify(ret.join('-')); diff --git a/packages/schematics/view/schema.d.ts b/packages/schematics/view/schema.d.ts deleted file mode 100644 index 68d019f83..000000000 --- a/packages/schematics/view/schema.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -export interface Schema { - /** - * The path to create the component. - */ - path?: string; - /** - * The name of the project. - */ - project?: string; - /** - * The name of the component. - */ - name: string; - /** - * Specifies if the style will be in the ts file. - */ - inlineStyle?: boolean; - /** - * Specifies if the template will be in the ts file. - */ - inlineTemplate?: boolean; - /** - * Specifies the view encapsulation strategy. - */ - viewEncapsulation?: 'Emulated' | 'Native' | 'None'; - /** - * Specifies the change detection strategy. - */ - changeDetection?: 'Default' | 'OnPush'; - /** - * The prefix to apply to generated selectors. - */ - prefix?: string; - /** - * The file extension to be used for style files. - */ - styleext?: string; - /** - * Specifies if a spec file is generated. - */ - spec?: boolean; - /** - * Flag to indicate if a dir is created. - */ - flat?: boolean; - /** - * Flag to skip the module import. - */ - skipImport?: boolean; - /** - * The selector to use for the component. - */ - selector?: string; - /** - * Allows specification of the declaring module. - */ - module?: string; - /** - * Specifies if declaring module exports the component. - */ - export?: boolean; - /** - * Without prefix to selectors - */ - withoutPrefix?: boolean; - /** - * Specifies relative path. - */ - target?: string; - /** - * Specifies using modal mode. - */ - modal?: boolean; -} diff --git a/packages/schematics/view/schema.json b/packages/schematics/view/schema.json index 519280ff5..704963f40 100644 --- a/packages/schematics/view/schema.json +++ b/packages/schematics/view/schema.json @@ -34,7 +34,8 @@ "inlineTemplate": { "description": "Specifies if the template will be in the ts file.", "type": "boolean", - "default": false + "default": false, + "alias": "t" }, "viewEncapsulation": { "description": "Specifies the view encapsulation strategy.", @@ -88,30 +89,40 @@ "format": "html-selector", "description": "The selector to use for the component." }, - "module": { - "type": "string", - "description": "Allows specification of the declaring module.", - "alias": "m" - }, "export": { "type": "boolean", "default": false, "description": "Specifies if declaring module exports the component." }, - "withoutPrefix": { + "entryComponent": { "type": "boolean", - "description": "Without prefix to selectors", - "default": false + "default": false, + "description": "Specifies if the component is an entry component of declaring module." + }, + "lintFix": { + "type": "boolean", + "default": false, + "description": "Specifies whether to apply lint fixes after generating the component." + }, + "module": { + "type": "string", + "description": "Allows specification of the declaring module. (e.g., -m=trade)", + "alias": "m" }, "target": { "type": "string", - "description": "Specifies relative path.", + "description": "指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.)", "alias": "t" }, + "withoutPrefix": { + "type": "boolean", + "description": "组件名不加前缀 (Without prefix to selectors)", + "default": false + }, "modal": { "type": "boolean", "default": true, - "description": "Specifies using modal mode." + "description": "指定是否使用模态框 (Specifies using modal mode)" } }, "required": [] diff --git a/packages/schematics/view/schema.ts b/packages/schematics/view/schema.ts new file mode 100644 index 000000000..fdcdceda9 --- /dev/null +++ b/packages/schematics/view/schema.ts @@ -0,0 +1,16 @@ +import {Schema as ComponentSchema} from '@schematics/angular/component/schema'; + +export interface Schema extends ComponentSchema { + /** + * 指定目标路径,支持 `bus/list` 写法 (Specifies relative path, could be set like `bus/list`.) + */ + target?: string; + /** + * 指定组件名不加前缀 (Without prefix to selectors) + */ + withoutPrefix?: boolean; + /** + * 指定是否使用模态框 (Specifies using modal mode) + */ + modal?: boolean; +} diff --git a/scripts/ci/build-schematics.sh b/scripts/ci/build-schematics.sh index a2a5f0507..5b5bbd175 100644 --- a/scripts/ci/build-schematics.sh +++ b/scripts/ci/build-schematics.sh @@ -60,9 +60,11 @@ copyFiles() { "${1}.prettierignore|${2}application/files/root/__dot__prettierignore" "${1}.prettierrc|${2}application/files/root/__dot__prettierrc" "${1}.stylelintrc|${2}application/files/root/__dot__stylelintrc" + # cli + "${1}_cli-tpl|${2}application/files/root/" # ci "${1}.vscode|${2}application/files/root/__dot__vscode" - "${1}scripts/color-less.js|${2}application/files/root/scripts" + "${1}scripts/color-less.js|${2}application/files/root/scripts/" # LICENSE "${1}LICENSE|${2}application/files/root" "${1}README.md|${2}application/files/root" @@ -73,6 +75,8 @@ copyFiles() { "${1}src/styles|${2}application/files/src/" "${1}src/main.ts|${2}application/files/src/" "${1}src/styles.less|${2}application/files/src/" + "${1}src/style-icons-auto.ts|${2}application/files/src/" + "${1}src/style-icons.ts|${2}application/files/src/" # assets "${1}src/assets/*.svg|${2}application/files/src/assets/" "${1}src/assets/*.less|${2}application/files/src/assets/"