Skip to content

Commit

Permalink
feat: get types from ts
Browse files Browse the repository at this point in the history
  • Loading branch information
ZakaryCode committed Nov 7, 2019
1 parent 780addf commit e0e1852
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 2 deletions.
61 changes: 61 additions & 0 deletions build/docs-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as fs from "fs"
import * as path from "path"
import * as ts from "typescript"
import { generateDocumentation, DocEntry } from "./parser"

export default function docsAPI (base: string = '.', out: string, files: string[]) {
const cwd: string = process.cwd();
const basepath: string = path.resolve(cwd, base);
files.forEach(async s => {
compile(cwd, s, (routepath, doc) => {
console.log(routepath, doc.length)
if (doc.length < 1) return
const outpath: string = routepath
.replace(basepath, path.resolve(cwd, out))
.replace(/(.[a-z]+)$|(.d.ts)$/ig, '')
try {
writeDoc(outpath, doc)
} catch (error) {
fs.mkdirSync(path.parse(outpath).dir, { recursive: true })
writeDoc(outpath, doc)
}
})
})
}

export function compile (p: string, n: string, callback?: (routepath: string, doc: DocEntry[]) => void) {
const route = path.resolve(p, n)
const stat = fs.statSync(route)
if (stat.isDirectory()) {
fs.readdirSync(route, {
encoding: 'utf8'
}).forEach(filename => ![
'node_modules', 'bin', 'templates', 'dist', '__tests__', '__mocks__', '_book', '.vscode', '.idea'
].includes(filename) && compile(route, filename, callback))
} else {
const docTree = generateDocumentation(route, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.ESNext
})
callback && callback(route, docTree)
}
}

export function writeJson (routepath: string, doc: DocEntry[]) {
fs.writeFileSync(
`${routepath}.json`,
JSON.stringify(doc, undefined, 4),
{}
)
}

export function writeDoc (routepath: string, doc: DocEntry[]) {
fs.writeFileSync(
`${routepath}.md`,
JSON.stringify(doc, undefined, 4),
{}
)
}

// docsAPI('.', process.argv[2], process.argv.slice(3))
docsAPI('./packages/taro/types/api', 'api', ['./packages/taro/types/api/'])
134 changes: 134 additions & 0 deletions build/parser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as ts from "typescript"

export interface DocEntry {
name?: string | ts.__String
fileName?: string
documentation?: string
jsTags?: ts.JSDocTagInfo[]
type?: string
constructors?: DocEntry[]
parameters?: DocEntry[]
returnType?: string
members?: DocEntry[]
exports?: DocEntry[]
}

export function generateDocumentation(
filepath: string,
options: ts.CompilerOptions
): DocEntry[] {
const program = ts.createProgram([filepath], options)
const checker = program.getTypeChecker()

const output: DocEntry[] = []

for (const sourceFile of program.getSourceFiles()) {
// if (!sourceFile.isDeclarationFile) {}
if (filepath === sourceFile.fileName) {
ts.forEachChild(sourceFile, visitAST)
}
}

return output

function visitAST(node: ts.Node) {
// Only consider exported nodes
if (!isNodeExported(node as ts.Declaration) || node.kind === ts.SyntaxKind.EndOfFileToken || node.kind === ts.SyntaxKind.DeclareKeyword
|| ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node) || ts.isImportClause(node)
|| ts.isExportAssignment(node) || ts.isExportDeclaration(node)
|| ts.isExpressionStatement(node) || ts.isEmptyStatement(node)
|| node.kind === ts.SyntaxKind.ExportKeyword) {
return
}

if (ts.isVariableDeclaration(node) || ts.isClassDeclaration(node) && node.name) {
const symbol = checker.getSymbolAtLocation(node)
symbol && output.push(serializeClass(symbol))
} else if (ts.isFunctionDeclaration(node)) {
const signature = checker.getSignatureFromDeclaration(node)
signature && output.push(serializeSignature(signature))
} else if (ts.isInterfaceDeclaration(node)) {
const symbol = checker.getTypeAtLocation(node).getSymbol()
symbol && output.push(serializeType(symbol, undefined, 'InterfaceDeclaration'))
} else if (ts.isTypeAliasDeclaration(node)) {
const symbol = checker.getTypeAtLocation(node).getSymbol()
symbol && output.push(serializeType(symbol, ts.idText(node.name), 'TypeAliasDeclaration'))
} else if (ts.isEnumDeclaration(node)) {
const symbol = checker.getTypeAtLocation(node).getSymbol()
symbol && output.push(serializeType(symbol))
} else if (ts.isIdentifier(node)) {
const symbol = checker.getTypeAtLocation(node).getSymbol()
symbol && output.push(serializeType(symbol))
} else if (ts.isModuleDeclaration(node) || ts.isModuleBlock(node) || ts.isVariableStatement(node)) {
// This is a namespace, visitAST its children
ts.forEachChild(node, visitAST)
} else if (ts.isVariableDeclarationList(node)) {
node.declarations.forEach(d => {
const symbol = d['symbol']
symbol && output.push(serializeType(symbol))
})
} else {
console.log(`WARN: Statement kind ${node.kind} is missing parse!\n\n${node.getText()}`)
}
}

/** Serialize a symbol into a json object */
function serializeSymbol(symbol: ts.Symbol, name?: string, type?: string): DocEntry {
return {
jsTags: symbol.getJsDocTags(),
name: name || symbol.getName(),
documentation: ts.displayPartsToString(symbol.getDocumentationComment(checker)),
type: type || checker.typeToString(
checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
)
}
}

/** Serialize a class symbol information */
function serializeClass(symbol: ts.Symbol) {
const details = serializeSymbol(symbol)
// Get the construct signatures
const constructorType = checker.getTypeOfSymbolAtLocation(
symbol,
symbol.valueDeclaration!
)
const signatures = constructorType.getConstructSignatures()
details.constructors = signatures.map(serializeSignature)
return details
}

/** Serialize a types (type or interface) symbol information */
function serializeType(symbol: ts.Symbol, name?: string, type?: keyof typeof ts.SyntaxKind): DocEntry {
// console.log(type, Object.keys(symbol))
const doc: DocEntry = serializeSymbol(symbol, name, type)
symbol.exports && symbol.exports.forEach((value) => {
if (!doc.exports) doc.exports = []
doc.exports.push(serializeSymbol(value))
})
symbol.members && symbol.members.forEach((value) => {
if (!doc.members) doc.members = []
doc.members.push(serializeSymbol(value))
})
return doc
}

/** Serialize a signature (call or construct) */
function serializeSignature(signature: ts.Signature) {
const typeParameters = signature.getTypeParameters() || []
return {
jsTags: signature.getJsDocTags(),
documentation: ts.displayPartsToString(signature.getDocumentationComment(checker)),
parameters: signature.getParameters().map((e, i) =>
serializeSymbol(e, undefined, typeParameters[i] && checker.typeToString(typeParameters[i]))),
returnType: checker.typeToString(signature.getReturnType())
}
}

/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Declaration): boolean {
return (
(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 ||
(!!node.parent/* && node.parent.kind === ts.SyntaxKind.SourceFile */)
)
}
}
178 changes: 178 additions & 0 deletions build/test-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* Documentation for C
*/
class C {
/**
* constructor documentation
* @param a my parameter documentation
* @param b another parameter documentation
*/
constructor(a: string, b: C) { }
}

/**
* for test
*/
export interface test {
a: string;
// test doc
b: InnerAudioContext;
}

type InnerAudioContext = {
/**
* 音频资源的地址,用于直接播放。2.2.3 开始支持云文件ID
*/
src: HTMLMediaElement['src']
// 开始播放的位置(单位:s),默认为 0
startTime: number
// 是否自动开始播放,默认为 false
autoplay: HTMLMediaElement['autoplay']
// 是否循环播放,默认为 false
loop: HTMLMediaElement['loop']
// 是否遵循系统静音开关,默认为 true。当此参数为 false 时,即使用户打开了静音开关,也能继续发出声音。从 2.3.0 版本开始此参数不生效,使用 wx.setInnerAudioOption 接口统一设置。
obeyMuteSwitch: boolean
// 音量。范围 0~1。默认为 1
volume: HTMLMediaElement['volume']
// 当前音频的长度(单位 s)。只有在当前有合法的 src 时返回(只读)
duration: HTMLMediaElement['duration']
// 当前音频的播放位置(单位 s)。只有在当前有合法的 src 时返回,时间保留小数点后 6 位(只读)
currentTime: HTMLMediaElement['currentTime']
// 当前是是否暂停或停止状态(只读)
paused: HTMLMediaElement['paused']
// 音频缓冲的时间点,仅保证当前播放时间点到此时间点内容已缓冲(只读)
buffered: HTMLMediaElement["buffered"]
// 播放
play: HTMLAudioElement["play"]
// 暂停。暂停后的音频再播放会从暂停处开始播放
pause: HTMLAudioElement["pause"]
// 停止。停止后的音频再播放会从头开始播放。
stop: () => void
// 跳转到指定位置
seek: (position: number) => void
/**
* 销毁当前实例
*/
destroy: () => void
// {(callback: function) => void} offCanplay(function callback) 取消监听音频进入可以播放状态的事件
// {(callback: function) => void} offEnded(function callback) 取消监听音频自然播放至结束的事件
// {(callback: function) => void} offError(function callback) 取消监听音频播放错误事件
// {(callback: function) => void} offPause(function callback) 取消监听音频暂停事件
// {(callback: function) => void} offPlay(function callback) 取消监听音频播放事件
// {(callback: function) => void} offSeeked(function callback) 取消监听音频完成跳转操作的事件
// {(callback: function) => void} offSeeking(function callback) 取消监听音频进行跳转操作的事件
// {(callback: function) => void} offStop(function callback) 取消监听音频停止事件
// {(callback: function) => void} offTimeUpdate(function callback) 取消监听音频播放进度更新事件
// {(callback: function) => void} offWaiting(function callback) 取消监听音频加载中事件
// {(callback: function) => void} onCanplay(function callback) 监听音频进入可以播放状态的事件。但不保证后面可以流畅播放
// {(callback: function) => void} onEnded(function callback) 监听音频自然播放至结束的事件
// {(callback: function) => void} onError(function callback) 监听音频播放错误事件
// {(callback: function) => void} onPause(function callback) 监听音频暂停事件
// {(callback: function) => void} onPlay(function callback) 监听音频播放事件
// {(callback: function) => void} onSeeked(function callback) 监听音频完成跳转操作的事件
// {(callback: function) => void} onSeeking(function callback) 监听音频进行跳转操作的事件
// {(callback: function) => void} onStop(function callback) 监听音频停止事件
// {(callback: function) => void} onTimeUpdate(function callback) 监听音频播放进度更新事件
// {(callback: function) => void} onWaiting(function callback) 监听音频加载中事件。当音频因为数据不足,需要停下来加载时会触发
}

/**
* 创建内部 audio 上下文 InnerAudioContext 对象。
*/
export const createInnerAudioContext = (): InnerAudioContext => {
let audioEl: HTMLAudioElement = new Audio()

const iac: InnerAudioContext = {
src: audioEl.src,
startTime: 0,
autoplay: audioEl.autoplay,
loop: audioEl.loop,
obeyMuteSwitch: false,
volume: audioEl.volume,
duration: audioEl.duration,
currentTime: audioEl.currentTime,
buffered: audioEl.buffered,
paused: audioEl.paused,
play: () => audioEl.play(),
pause: () => audioEl.pause(),
stop: () => {
iac.pause()
iac.seek(0)
},
seek: position => {
audioEl.currentTime = position
},
/**
* @todo destroy 得并不干净
*/
destroy: () => {
iac.stop()
document.body.removeChild(audioEl)
audioEl = null
}
}

const simpleProperties = [ 'src', 'autoplay', 'loop', 'volume', 'duration', 'currentTime', 'buffered', 'paused' ]
simpleProperties.forEach(propertyName => {
Object.defineProperty(iac, propertyName, {
get: () => audioEl[propertyName],
set (value) { audioEl[propertyName] = value }
})
})

const simpleEvents = [
'Canplay',
'Ended',
'Pause',
'Play',
'Seeked',
'Seeking',
'TimeUpdate',
'Waiting'
]
const simpleListenerTuples = [
['on', audioEl.addEventListener],
['off', audioEl.removeEventListener]
]

simpleEvents.forEach(eventName => {
simpleListenerTuples.forEach(([eventNamePrefix, listenerFunc]: any) => {
Object.defineProperty(iac, `${eventNamePrefix}${eventName}`, {
get () {
return callback => listenerFunc.call(audioEl, eventName.toLowerCase(), callback)
}
})
})
})

const customEvents = [ 'Stop', 'Error' ]
const customListenerTuples = [
['on', 'add'],
['off', 'remove']
]

customEvents.forEach(eventName => {
customListenerTuples.forEach(([eventNamePrefix, actionName]) => {
Object.defineProperty(iac, `${eventNamePrefix}${eventName}`, {
get () {}
})
})
})

return iac
}

/**
* @typedef {object} AudioContext
* @property {(src: string) => void} setSrc 设置音频地址
* @property {() => void} play 播放音频。
* @property {() => void} pause 暂停音频。
* @property {(position: number) => void} seek(number position) 跳转到指定位置。
*/

/**
* 创建 audio 上下文 AudioContext 对象。
* @param {string} id <audio> 组件的 id
* @param {Object} this 在自定义组件下,当前组件实例的this,以操作组件内 <audio> 组件
* @returns {AudioContext}
*/
6 changes: 6 additions & 0 deletions build/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "umd"
}
}
Loading

0 comments on commit e0e1852

Please sign in to comment.