diff --git a/tools/missing-types.json b/tools/missing-types.json new file mode 100644 index 00000000..b1c0fada --- /dev/null +++ b/tools/missing-types.json @@ -0,0 +1,68 @@ +{ + "values": [ + { + "name": "Component", + "path": "devextreme/core/component" + }, + { + "name": "Widget", + "path": "devextreme/ui/widget/ui.widget" + }, + { + "name": "CollectionWidget", + "path": "devextreme/ui/collection/ui.collection_widget.base" + }, + { + "name": "dxActionSheet", + "path": "devextreme/ui/action_sheet" + }, + { + "name": "dxButton", + "path": "devextreme/ui/button" + }, + { + "name": "dxDataGrid", + "path": "devextreme/ui/data_grid" + }, + { + "name": "dxDraggable", + "path": "devextreme/ui/draggable" + }, + { + "name": "dxDropDownButton", + "path": "devextreme/ui/drop_down_button" + }, + { + "name": "dxFilterBuilder", + "path": "devextreme/ui/filter_builder" + }, + { + "name": "dxForm", + "path": "devextreme/ui/form" + }, + { + "name": "dxOverlay", + "path": "devextreme/ui/overlay" + }, + { + "name": "dxPopup", + "path": "devextreme/ui/popup" + }, + { + "name": "dxSortable", + "path": "devextreme/ui/sortable" + }, + { + "name": "dxTabPanel", + "path": "devextreme/ui/tab_panel" + }, + { + "name": "dxTextEditor", + "path": "devextreme/ui/text_box/ui.text_editor.base" + }, + { + "name": "dxTreeList", + "path": "devextreme/ui/tree_list" + } + ] +} \ No newline at end of file diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts index d8324f62..3e5037e8 100644 --- a/tools/src/component-generator.ts +++ b/tools/src/component-generator.ts @@ -7,7 +7,7 @@ import { uppercaseFirst } from "./helpers"; -type IComponent = { +type IComponent = { name: string; baseComponentPath: string; extensionComponentPath: string; @@ -45,6 +45,7 @@ interface ISubscribableOption { type IOption = { name: string; isSubscribable?: true; + isArray?: boolean } & ({ type: string; nested?: undefined; @@ -78,10 +79,10 @@ function generateReExport(path: string, fileName: string): string { return renderReExport({ path, fileName }); } -const renderReExport: (model: {path: string, fileName: string}) => string = createTempate( -`/** @deprecated Use 'devextreme-react/<#= it.fileName #>' file instead */\n` + -`export * from "<#= it.path #>";\n` + -`export { default } from "<#= it.path #>";\n` +const renderReExport: (model: { path: string, fileName: string }) => string = createTempate( + `/** @deprecated Use 'devextreme-react/<#= it.fileName #>' file instead */\n` + + `export * from "<#= it.path #>";\n` + + `export { default } from "<#= it.path #>";\n` ); function generate(component: IComponent): string { @@ -172,7 +173,7 @@ function generate(component: IComponent): string { : undefined; const hasExtraOptions = !component.isExtension; - const widgetName = `dx${uppercaseFirst(component.name)}`; + const widgetName = `dx${uppercaseFirst(component.name)}`; const renderedPropTypings = component.propTypings ? component.propTypings @@ -227,13 +228,13 @@ function buildPropsTypeName(className: string) { function createTemplateDto(templates: string[] | undefined) { return templates - ? templates.map((actualOptionName) => ({ - actualOptionName, - render: formatTemplatePropName(actualOptionName, "Render"), - component: formatTemplatePropName(actualOptionName, "Component"), - keyFn: formatTemplatePropName(actualOptionName, "KeyFn") - })) - : undefined; + ? templates.map((actualOptionName) => ({ + actualOptionName, + render: formatTemplatePropName(actualOptionName, "Render"), + component: formatTemplatePropName(actualOptionName, "Component"), + keyFn: formatTemplatePropName(actualOptionName, "KeyFn") + })) + : undefined; } function formatTemplatePropName(name: string, suffix: string): string { @@ -261,23 +262,23 @@ const renderModule: (model: { defaultExport: string; renderedExports: string; }) => string = createTempate( -`<#= it.renderedImports #>` + `\n` + + `<#= it.renderedImports #>` + `\n` + -`<#? it.renderedOptionsInterface #>` + + `<#? it.renderedOptionsInterface #>` + `<#= it.renderedOptionsInterface #>` + `\n` + `\n` + -`<#?#>` + + `<#?#>` + -`<#= it.renderedComponent #>` + + `<#= it.renderedComponent #>` + -`<#? it.renderedNestedComponents #>` + + `<#? it.renderedNestedComponents #>` + `// tslint:disable:max-classes-per-file` + `<#~ it.renderedNestedComponents :nestedComponent #>` + `\n\n` + - `<#= nestedComponent #>` + + `<#= nestedComponent #>` + `<#~#>` + `\n\n` + -`<#?#>` + + `<#?#>` + -`export default <#= it.defaultExport #>;` + `\n` + -`export { + `export default <#= it.defaultExport #>;` + `\n` + + `export { <#= it.renderedExports #> }; `); @@ -293,23 +294,23 @@ const renderImports: (model: { hasPropTypings: boolean; configComponentPath?: string; }) => string = createTempate( -`import <#= it.widgetName #>, { + `import <#= it.widgetName #>, { IOptions` + `<#? it.optionsAliasName #> as <#= it.optionsAliasName #><#?#>` + `\n` + -`} from "<#= it.dxExportPath #>";` + `\n` + `\n` + + `} from "<#= it.dxExportPath #>";` + `\n` + `\n` + -`<#? it.hasPropTypings #>` + + `<#? it.hasPropTypings #>` + `import * as PropTypes from "prop-types";` + `\n` + -`<#?#>` + + `<#?#>` + -`import { <#= it.baseComponentName #> as BaseComponent` + + `import { <#= it.baseComponentName #> as BaseComponent` + `<#? it.hasExtraOptions #>` + - `, IHtmlOptions` + + `, IHtmlOptions` + `<#?#>` + -` } from "<#= it.baseComponentPath #>";` + `\n` + + ` } from "<#= it.baseComponentPath #>";` + `\n` + -`<#? it.configComponentPath #>` + + `<#? it.configComponentPath #>` + `import NestedOption from "<#= it.configComponentPath #>";` + `\n` + -`<#?#>` + `<#?#>` ); const renderOptionsInterface: (model: { @@ -323,19 +324,19 @@ const renderOptionsInterface: (model: { type: string; }>; }) => string = createTempate( -`interface <#= it.optionsName #> extends IOptions, IHtmlOptions {` + `\n` + + `interface <#= it.optionsName #> extends IOptions, IHtmlOptions {` + `\n` + -`<#~ it.templates :template #>` + + `<#~ it.templates :template #>` + ` <#= template.render #>?: ${TYPE_RENDER};` + `\n` + ` <#= template.component #>?: ${TYPE_COMPONENT};` + `\n` + ` <#= template.keyFn #>?: ${TYPE_KEY_FN};` + `\n` + -`<#~#>` + + `<#~#>` + -`<#~ it.defaultProps :prop #>` + + `<#~ it.defaultProps :prop #>` + ` <#= prop.name #>?: <#= prop.type #>;` + `\n` + -`<#~#>` + + `<#~#>` + -`}` + `}` ); const renderComponent: (model: { @@ -347,7 +348,7 @@ const renderComponent: (model: { renderedTemplateProps?: string[]; renderedPropTypings?: string[]; }) => string = createTempate( -`class <#= it.className #> extends BaseComponent<<#= it.optionsName #>> { + `class <#= it.className #> extends BaseComponent<<#= it.optionsName #>> { public get instance(): <#= it.widgetName #> { return this._instance; @@ -355,31 +356,31 @@ const renderComponent: (model: { protected _WidgetClass = <#= it.widgetName #>;\n` + -`<#? it.renderedDefaultProps #>` + -L1 + `protected _defaults = {<#= it.renderedDefaultProps.join(',') #>` + -L1 + `};\n` + -`<#?#>` + + `<#? it.renderedDefaultProps #>` + + L1 + `protected _defaults = {<#= it.renderedDefaultProps.join(',') #>` + + L1 + `};\n` + + `<#?#>` + -`<#? it.expectedChildren #>` + -L1 + `protected _expectedChildren = {` + + `<#? it.expectedChildren #>` + + L1 + `protected _expectedChildren = {` + -`<#~ it.expectedChildren : child #>` + -L2 + `<#= child.componentName #>:` + + `<#~ it.expectedChildren : child #>` + + L2 + `<#= child.componentName #>:` + ` { optionName: "<#= child.optionName #>", isCollectionItem: <#= !!child.isCollectionItem #> },` + -`<#~#>` + `\b` + + `<#~#>` + `\b` + -L1 + `};\n` + -`<#?#>` + + L1 + `};\n` + + `<#?#>` + -`<#? it.renderedTemplateProps #> + `<#? it.renderedTemplateProps #> protected _templateProps = [<#= it.renderedTemplateProps.join(', ') #>]; <#?#>}` + `\n` + -`<#? it.renderedPropTypings #>` + + `<#? it.renderedPropTypings #>` + `(<#= it.className #> as any).propTypes = {` + `\n` + - `<#= it.renderedPropTypings.join(',\\n') #>` + `\n` + + `<#= it.renderedPropTypings.join(',\\n') #>` + `\n` + `};` + `\n` + -`<#?#>` + `<#?#>` ); const renderNestedComponent: (model: { @@ -397,49 +398,49 @@ const renderNestedComponent: (model: { renderedTemplateProps?: string[]; owners: string[]; }) => string = createTempate( -`// owners:\n` + -`<#~ it.owners : owner #>` + + `// owners:\n` + + `<#~ it.owners : owner #>` + `// <#= owner #>\n` + -`<#~#>` + + `<#~#>` + -`interface <#= it.propsType #> <#= it.renderedType #>\n` + + `interface <#= it.propsType #> <#= it.renderedType #>\n` + -`class <#= it.className #> extends NestedOption<<#= it.propsType #>> {` + -L1 + `public static OptionName = "<#= it.optionName #>";` + + `class <#= it.className #> extends NestedOption<<#= it.propsType #>> {` + + L1 + `public static OptionName = "<#= it.optionName #>";` + -`<#? it.isCollectionItem #>` + + `<#? it.isCollectionItem #>` + L1 + `public static IsCollectionItem = true;` + -`<#?#>` + + `<#?#>` + -`<#? it.renderedSubscribableOptions #>` + + `<#? it.renderedSubscribableOptions #>` + L1 + `public static DefaultsProps = {<#= it.renderedSubscribableOptions.join(',') #>` + L1 + `};` + -`<#?#>` + + `<#?#>` + -`<#? it.expectedChildren #>` + + `<#? it.expectedChildren #>` + L1 + `public static ExpectedChildren = {` + `<#~ it.expectedChildren : child #>` + L2 + `<#= child.componentName #>:` + - ` { optionName: "<#= child.optionName #>", isCollectionItem: <#= !!child.isCollectionItem #> },` + + ` { optionName: "<#= child.optionName #>", isCollectionItem: <#= !!child.isCollectionItem #> },` + `<#~#>` + `\b` + - L1 + `};` + -`<#?#>` + + L1 + `};` + + `<#?#>` + -`<#? it.renderedTemplateProps #>` + + `<#? it.renderedTemplateProps #>` + L1 + `public static TemplateProps = [<#= it.renderedTemplateProps.join(', ') #>];` + -`<#?#>` + + `<#?#>` + -`<#? it.predefinedProps #>` + + `<#? it.predefinedProps #>` + L1 + `public static PredefinedProps = {` + - `<#~ it.predefinedProps : prop #>` + - L2 + `<#= prop.name #>: "<#= prop.value #>",` + - `<#~#>` + `\b` + + `<#~ it.predefinedProps : prop #>` + + L2 + `<#= prop.name #>: "<#= prop.value #>",` + + `<#~#>` + `\b` + L1 + `};` + -`<#?#>` + + `<#?#>` + -`\n}` + `\n}` ); const renderTemplateOption: (model: { @@ -457,17 +458,17 @@ const renderTemplateOption: (model: { // tslint:disable:max-line-length const renderPropTyping: (model: IRenderedPropTyping) => string = createTempate( -` <#= it.propName #>: ` + + ` <#= it.propName #>: ` + -`<#? it.renderedTypes.length === 1 #>` + + `<#? it.renderedTypes.length === 1 #>` + `<#= it.renderedTypes[0] #>` + -`<#??#>` + + `<#??#>` + `PropTypes.oneOfType([` + `\n` + ` <#= it.renderedTypes.join(',\\n ') #>` + `\n` + ` ])` + -`<#?#>` + `<#?#>` ); // tslint:enable:max-line-length @@ -485,16 +486,18 @@ function renderObject(props: IOption[], indent: number): string { props.forEach((opt) => { result += "\n" + getIndent(indent) + opt.name + "?: "; + if (opt.nested && isNotEmptyArray(opt.nested)) { result += renderObject(opt.nested, indent); } else { result += opt.type; } + if (opt.isArray) { result += '[]' } result += ";"; }); indent -= 1; - result += "\n" + getIndent(indent) + "}"; + result += "\n" + getIndent(indent) + "}"; return result; } diff --git a/tools/src/generator.ts b/tools/src/generator.ts index 712056e6..26c899a3 100644 --- a/tools/src/generator.ts +++ b/tools/src/generator.ts @@ -1,5 +1,7 @@ import { writeFileSync as writeFile } from "fs"; +const missingImports = require('../missing-types.json') + import { dirname as getDirName, join as joinPaths, @@ -9,9 +11,12 @@ import { } from "path"; import { + IArrayDescr, IComplexProp, ICustomType, + IFunctionDescr, IModel, + IObjectDescr, IProp, ITypeDescr, IWidget @@ -20,6 +25,9 @@ import { import { convertTypes } from "./converter"; import generateIndex, { IReExport } from "./index-generator"; +const generatedInterfaces = { values: new Set() } +const importCustomTypesSet = { values: new Set() } + import generateComponent, { generateReExport, IComponent, @@ -31,6 +39,7 @@ import generateComponent, { import { isEmptyArray, + isNotEmptyArray, removeExtension, removePrefix, toKebabCase, @@ -58,6 +67,11 @@ function generate({ }) { const modulePaths: IReExport[] = []; + const SAMPLE = parseCustomTypes(rawData.customTypes) + const importStr = missingImports.values.map((imp) => `import ${imp.name} from "${imp.path}";`).join('\n') + '\nimport {GridBase} from "devextreme/ui/data_grid"\n'; + const exportStr = missingImports.values.map((imp) => `export {${imp.name}};`).join('\n') + 'export {GridBase}'; + writeFile('./src/types.d.ts', importStr + '\n' + SAMPLE + exportStr); + rawData.widgets.forEach((data) => { const widgetFile = mapWidget( data, @@ -70,7 +84,7 @@ function generate({ const widgetFilePath = joinPaths(out.componentsDir, widgetFile.fileName); const indexFileDir = getDirName(out.indexFileName); - writeFile(widgetFilePath, generateComponent(widgetFile.component), { encoding: "utf8" }); + writeFile(widgetFilePath, `import * as types from "./types"\n` + generateComponent(widgetFile.component), { encoding: "utf8" }); modulePaths.push({ name: widgetFile.component.name, path: "./" + removeExtension(getRelativePath(indexFileDir, widgetFilePath)).replace(pathSeparator, "/") @@ -87,6 +101,7 @@ function generate({ }); writeFile(out.indexFileName, generateIndex(modulePaths), { encoding: "utf8" }); + } function mapWidget( @@ -98,13 +113,16 @@ function mapWidget( widgetPackage: string ): { fileName: string; - component: IComponent + component: IComponent; + importCustomTypes: string[] } { const name = removePrefix(raw.name, "dx"); const subscribableOptions: ISubscribableOption[] = raw.options .filter((o) => o.isSubscribable) .map(mapSubscribableOption); + importCustomTypesSet.values = new Set(); + const nestedOptions = raw.complexOptions ? extractNestedComponents(raw.complexOptions, raw.name, name) : null; @@ -115,6 +133,9 @@ function mapWidget( }, {}); const propTypings = extractPropTypings(raw.options, customTypeHash); + const importsSet = new Set( + [...importCustomTypesSet.values].filter(x => generatedInterfaces.values.has(x))); + return { fileName: `${toKebabCase(name)}.ts`, component: { @@ -129,7 +150,8 @@ function mapWidget( nestedComponents: nestedOptions && nestedOptions.length > 0 ? nestedOptions : undefined, expectedChildren: raw.nesteds, propTypings: propTypings.length > 0 ? propTypings : undefined - } + }, + importCustomTypes: Array.from(importsSet) as string[] }; } @@ -141,6 +163,7 @@ function extractNestedComponents(props: IComplexProp[], rawWidgetName: string, w }); return props.map((p) => { + if (p.name === "groupItems") debugger; return { className: nameClassMap[p.name], owners: p.owners.map((o) => nameClassMap[o]), @@ -187,19 +210,24 @@ function createPropTyping(option: IProp, customTypes: Record { + return customTypeToString(type) + }) + return customDeclaration.join('\n') +} + +function _typeToStr(type: ITypeDescr, nested: Boolean = false): string { + if (type.acceptableValues) + return type.acceptableValues.join('|') + if (type.type == 'Any') + return 'any' + if (type.isCustomType) { + importCustomTypesSet.values.add(type.type) + if (nested) { + return 'types.' + type.type.replace(/\./g, '') + } + } + + return type.type.replace(/\./g, '') +} + +function _typeArrToStr(type_arr: IArrayDescr, nested: Boolean = false): string { + if (isNotEmptyArray(type_arr.itemTypes)) { + return `Array<${type_arr.itemTypes.map(t => ITypeDescrToStr(t, nested)).join('|')}>` + } + +} + +function _typeFuncToStr(func: IFunctionDescr, nested: Boolean = false): string { + if (isNotEmptyArray(func.params)) { + const declarations = func.params.map(p => { + const types = p.types.map(t => ITypeDescrToStr(t, nested)) + return `${p.name}: ${types.join('|')}` + }); + const ret = ITypeDescrToStr(func.returnValueType, nested); + return `((${declarations.join(',')})=>${ret})` + } + else return 'Function' +} + +function _typeObjToStr(obj: IObjectDescr, nested: Boolean = false): string { + if (isNotEmptyArray(obj.fields)) { + const fields = obj.fields.map(f => { + const types = f.types.map(t => ITypeDescrToStr(t, nested)) + return `${f.name}: ${types.join('|')}` + }) + return `{${fields.join(',\n')}}`; + } + else return 'Object'; + +} + +function ITypeDescrToStr( + basic_type: ITypeDescr | IArrayDescr | IFunctionDescr | IObjectDescr, + nested: Boolean = false +) { + switch (basic_type.type) { + case ('Object'): + return _typeObjToStr(basic_type as IObjectDescr, nested) + case ('Function'): + return _typeFuncToStr(basic_type as IFunctionDescr, nested) + case ('Array'): + return _typeArrToStr(basic_type as IArrayDescr, nested) + default: + return _typeToStr(basic_type, nested) + } +} + +function typePropToStr(prop: IProp, noname: Boolean = false, nested: Boolean = false): string { + const name = noname ? '' : `${prop.name.replace(/\(.*\)/, '')}: ` + + const test = nestedOptionArrayPostfix(prop) + if (prop.name === "groupItems") debugger; + if (isNotEmptyArray(prop.props)) { + return `${name}{${prop.props.map(p => typePropToStr(p, false, nested))}}` + + } + else { + if (isNotEmptyArray(prop.types)) { + return `${name}${prop.types.map(p => ITypeDescrToStr(p, nested)).join('|')}` + } + else return `${name}any` + } +} + +function customTypeToString(type: ICustomType): string { + const interfaceName = type.name.replace(/\./g, '') + generatedInterfaces.values.add(interfaceName) + const statements = [] + let prev = '' + let tempStatements = [] + if (isNotEmptyArray(type.props)) { + type.props.map(p => { + const name = p.name.replace(/\(.*\)/, '') + if (name !== prev) { + statements.push([...tempStatements, typePropToStr(p)].reverse().join('|')) + prev = name + tempStatements = [] + } + else { + tempStatements.push(typePropToStr(p, true)) + } + }) + return `export interface ${interfaceName} { + ${statements.map((s) => '\t' + s).join(',\n')} + }\n`; + } + else return `export interface ${interfaceName}{}` + +} + +export default generate; \ No newline at end of file