Skip to content

Commit

Permalink
Merge pull request #12441 from robinv8/dev-types
Browse files Browse the repository at this point in the history
refactor(types): 根据各个小程序官方文档,同步组件类型声明
  • Loading branch information
ZakaryCode authored Sep 14, 2022
2 parents d94ae75 + b701416 commit 61ce18d
Show file tree
Hide file tree
Showing 54 changed files with 2,399 additions and 648 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export interface RadioState {
export interface RadioProps extends _RadioProps {
style?: StyleProp<ViewStyle>|any;
/* rn独有的,在taro里面看不到,得翻源码才能看到 **/
onChange?: (evt: EventOnChange) => void;
// onChange?: (evt: EventOnChange) => void;
}
2 changes: 2 additions & 0 deletions packages/taro-components-rn/src/components/Video/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ global._taroVideoMap = {}

interface Props extends VideoProps {
onLoad: () => void;
// 兼容旧版本,可传入 style 对象
style?: any;
}

class _Video extends Component<Props, any> {
Expand Down
6 changes: 5 additions & 1 deletion packages/taro-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"dependencies": {
"@stencil/core": "~2.13.0",
"@stencil/sass": "1.5.2",
"@tarojs/taro": "workspace:*",
"@tarojs/router": "workspace:*",
"@tarojs/taro": "workspace:*",
"better-scroll": "^1.14.1",
"classnames": "^2.2.5",
"hls.js": "^1.1.5",
Expand All @@ -52,9 +52,13 @@
"weui": "^1.1.2"
},
"devDependencies": {
"@types/node": "^14.14.31",
"css-loader": "3.4.2",
"humps": "^2.0.1",
"jquery": "^3.4.1",
"karmatic": "^2.1.0",
"lodash": "^4.17.21",
"miniapp-types": "1.1.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"simulant": "^0.2.2",
Expand Down
241 changes: 241 additions & 0 deletions packages/taro-components/src/utils/json-schema-to-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import generator from '@babel/generator'
import * as parser from '@babel/parser'
import traverse from '@babel/traverse'
import * as t from '@babel/types'
import * as fs from 'fs'
import * as humps from 'humps'
import { flattenDeep, isEmpty, toArray, xorWith } from 'lodash'
import * as path from 'path'
import { format as prettify } from 'prettier'

const MINI_APP_TYPES = ['weapp', 'alipay', 'swan', 'tt', 'qq', 'jd'] as const

const OMIT_PROPS = ['generic:simple-component']
class GenerateTypes {
jsonSchemas: any = {}
componentName
constructor (componentName) {
this.componentName = componentName

MINI_APP_TYPES.forEach((type) => {
try {
const json = require(`miniapp-types/dist/schema/${type}/${
componentName === 'AD' ? 'ad' : humps.decamelize(componentName, { separator: '-' })
}.json`)

if (!json) {
return
}
if (!this.jsonSchemas[componentName]) {
this.jsonSchemas[componentName] = {}
}
this.jsonSchemas[componentName][type] = json
} catch (error) {
// console.log(error)
if (!this.jsonSchemas[componentName]) {
this.jsonSchemas[componentName] = {}
}
}
})
}

// 获取不存在的属性
getMissingProps (props: { [key in typeof MINI_APP_TYPES[number]]?: string[] }) {
const obj = {}
const jsonSchema = this.jsonSchemas[this.componentName]
if (!jsonSchema) {
return obj
}
Object.keys(this.jsonSchemas[this.componentName]).forEach((key) => {
const filteredList = xorWith(props[key], Object.keys(this.jsonSchemas[this.componentName][key].properties))
if (filteredList.length > 0) {
obj[key] = filteredList.map((item) =>
item.match(/^bind/) ? humps.camelize(item.replace(/^bind/, 'on')) : item
)
}
})

return obj
}

// 转换不存在的属性,便于添加到已有的类型声明中
convertProps (props) {
const array = [...new Set(flattenDeep(toArray(props)))]
const reverseProps = {}
array.forEach((prop) => {
reverseProps[prop] = Object.keys(props).filter((key) => props[key].includes(prop))
})
return reverseProps
}

updateComment (ast) {
const componentName = this.componentName
const jsonSchemas = this.jsonSchemas[this.componentName]
const existProps: { [key in typeof MINI_APP_TYPES[number]]?: string[] } = {}

traverse(ast, {
TSInterfaceDeclaration (astPath) {
if (astPath.node.id.name !== `${componentName}Props`) {
return
}
astPath.traverse({
TSPropertySignature (astPath) {
const { name } = astPath.node.key as any
if (!name) {
return
}
const supportedPlatforms: string[] = []

const covertedName = name.match(/^on/)
? name.replace(/^on/, 'bind')
: humps.decamelize(name, { separator: '-' })
MINI_APP_TYPES.forEach((type) => {
if (jsonSchemas[type]?.properties[name]) {
if (isEmpty(existProps[type])) {
existProps[type] = [name]
}
existProps[type]?.push(name)
supportedPlatforms.push(type)
}

if (name !== covertedName && jsonSchemas[type]?.properties[covertedName]) {
if (isEmpty(existProps[type])) {
existProps[type] = [covertedName]
}
existProps[type]?.push(covertedName)
supportedPlatforms.push(type)
}
})
if (isEmpty(astPath.node.leadingComments) || !astPath.node.leadingComments?.[0]?.value) {
return
}
const value = astPath.node.leadingComments?.[0]?.value

// 保留原有 h5 类型
if (value.toLowerCase().indexOf('h5') > -1) {
supportedPlatforms.push('h5')
}

// 保留原有 rn 类型
if (value.toLowerCase().indexOf('rn') > -1) {
supportedPlatforms.push('rn')
}
if (isEmpty(supportedPlatforms)) {
astPath.remove()
} else {
astPath.node.leadingComments[0].value = value.replace(
/@supported .*?\n/,
`@supported ${supportedPlatforms.join(', ')}\n`
)
if (value.match(/@deprecated/)) {
astPath.node.leadingComments[0].value = value.replace(/\* @deprecated.*?\n/, '')
}
}
},
})
},
})
return {
existProps,
ast,
}
}

// 添加不存在的属性
addProps (ast, props) {
const componentName = this.componentName
const jsonSchemas = this.jsonSchemas[this.componentName]
traverse(ast, {
TSInterfaceDeclaration (astPath) {
if (astPath.node.id.name !== `${componentName}Props`) {
return
}
const addedProps: string[] = []
astPath.traverse({
TSInterfaceBody (astPath) {
Object.keys(props).forEach((prop) => {
if (OMIT_PROPS.includes(prop)) {
return
}
const node = t.cloneNode(astPath.node.body[0]) as t.TSPropertySignature
node.key = t.identifier(humps.camelize(prop, { separator: '-' }))
const platform = props[prop][0]
const json = jsonSchemas[platform]
const propSchema = json.properties[prop] || json.properties[prop.replace(/^on/, 'bind')]
const { type, tsType, enum: enumArray } = propSchema
let value
if (type === 'string') {
if (!enumArray) {
value = t.tsTypeReference(t.identifier(type))
} else {
value = t.tsUnionType(enumArray.map((item) => t.tsLiteralType(t.stringLiteral(item))))
}
} else if (type instanceof Array) {
value = t.tsTypeReference(t.identifier(type.join('|')))
} else if (tsType === '() => void') {
value = t.tsTypeReference(t.identifier('CommonEventFunction'))
} else {
value = t.tsTypeReference(t.identifier('string'))
}
node.typeAnnotation = t.tsTypeAnnotation(value)
node.optional = !json.required?.[prop] || !json.required?.[prop.replace(/^on/, 'bind')]

if (node.leadingComments) {
let commentValue = `* ${propSchema.description?.replace(/\n/g, '\n * ')} \n`
commentValue += `* @supported ${props[prop].join(', ')}\n`
const defaultValue = propSchema.defaultValue
if (defaultValue) {
if (defaultValue instanceof Array) {
commentValue += `* @default ${propSchema.defaultValue.join(',')}\n`
} else {
commentValue += `* @default ${propSchema.defaultValue}\n`
}
}
node.leadingComments[0].value = commentValue
}
astPath.node.body.push(node)
addedProps.push(prop)
})
},
})
},
})
}

formatJSDoc (ast) {
traverse(ast, {
enter (astPath) {
if (astPath.node.trailingComments) {
astPath.node.trailingComments = []
}
},
})
}

exec () {
const filePath = path.join(process.cwd(), 'types', `${this.componentName}.d.ts`)
const codeStr = fs.readFileSync(filePath, 'utf8')
const ast = parser.parse(codeStr, {
sourceType: 'module',
strictMode: false,
plugins: ['typescript'],
})
const { existProps } = this.updateComment(ast)
const missingProps = this.getMissingProps(existProps)
const props = this.convertProps(missingProps)
this.addProps(ast, props)
this.formatJSDoc(ast)
const result = generator(ast)
const code = prettify(result.code, { parser: 'typescript', singleQuote: true, semi: false })
fs.writeFileSync(filePath, code)
}
}
const typesFiles: string[] = fs.readdirSync(path.join(process.cwd(), 'types'))

typesFiles.forEach((fileName) => {
const componentName = fileName.replace(/\.d\.ts$/, '')
const generateTypes = new GenerateTypes(componentName)
generateTypes.exec()
})

export default GenerateTypes
Loading

0 comments on commit 61ce18d

Please sign in to comment.