From e0e1852790139e55ff287cca285ee7528f3adc14 Mon Sep 17 00:00:00 2001 From: ZakaryCode Date: Mon, 4 Nov 2019 11:03:44 +0800 Subject: [PATCH] feat: get types from ts --- build/docs-api.ts | 61 +++++++++++++++ build/parser/index.ts | 134 +++++++++++++++++++++++++++++++ build/test-file.ts | 178 ++++++++++++++++++++++++++++++++++++++++++ build/tsconfig.json | 6 ++ package.json | 7 +- tsconfig.json | 40 ++++++++++ 6 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 build/docs-api.ts create mode 100644 build/parser/index.ts create mode 100644 build/test-file.ts create mode 100644 build/tsconfig.json create mode 100644 tsconfig.json diff --git a/build/docs-api.ts b/build/docs-api.ts new file mode 100644 index 000000000000..19b3c537c567 --- /dev/null +++ b/build/docs-api.ts @@ -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/']) diff --git a/build/parser/index.ts b/build/parser/index.ts new file mode 100644 index 000000000000..c1cffec645ef --- /dev/null +++ b/build/parser/index.ts @@ -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 */) + ) + } +} diff --git a/build/test-file.ts b/build/test-file.ts new file mode 100644 index 000000000000..eaa817397d3a --- /dev/null +++ b/build/test-file.ts @@ -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