Created
September 28, 2016 08:07
-
-
Save maolion/5fee8364df06eecdc61fe8076f588ddc to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const FS = require("fs-extra"); | |
| const Path = require("path"); | |
| const Events = require("events"); | |
| const Chokidar = require('chokidar'); | |
| const Promise = require("thenfail").Promise; | |
| const Chalk = require('chalk'); | |
| const TS = require("typescript"); | |
| const Minimatch = require("minimatch").Minimatch; | |
| const Utils = require('./utils'); | |
| const Constants = require("../constants"); | |
| const PROJECT_DIR = Constants.SOURCE_DIR; | |
| const OUTPUT_FILE_PATH = Path.join(Constants.MAO_APP_TYPINGS_DIR, 'actions-x.d.ts'); | |
| const OPTIONS = { | |
| projectDir: PROJECT_DIR, | |
| compilerOptions: { | |
| target: TS.ScriptTarget.ES2015, | |
| module: TS.ModuleKind.CommonJS, | |
| noErrorTruncation: true | |
| }, | |
| include: [ | |
| "../typings/**/*.{ts,tsx}", | |
| "./actions/**/*.{ts,tsx}", | |
| "./models/**/*.{ts,tsx}" | |
| ], | |
| exclude: [ | |
| "../typings/**/*.{ts,tsx}", | |
| "actions/index.ts", | |
| "models/**/*.{ts,tsx}" | |
| ].map(item => new Minimatch(Path.resolve(PROJECT_DIR, item))) | |
| }; | |
| class Parser { | |
| constructor(program, checker) { | |
| this._program = program; | |
| this._checker = checker; | |
| this._symbolSet = new Set(); | |
| } | |
| parse(node) { | |
| const entries = []; | |
| TS.forEachChild(node, this._visit.bind(this, entries)); | |
| return Parser.entriesToString(entries); | |
| } | |
| _visit(entries, node) { | |
| if (!(node.flags & TS.NodeFlags.Export)) { | |
| return; | |
| } | |
| switch (node.kind) { | |
| case TS.SyntaxKind.ClassDeclaration: | |
| let name = node.name; | |
| if (name) { | |
| let symbol = this._checker.getSymbolAtLocation(name); | |
| if (this._symbolSet.has(symbol)) { | |
| return; | |
| } | |
| this._symbolSet.add(symbol); | |
| if (this._isActionModule(symbol)) { | |
| entries.push(this._serializeClass(symbol)) | |
| } | |
| } | |
| break; | |
| case TS.SyntaxKind.ModuleDeclaration: | |
| TS.forEachChild(node, this._visit.bind(this, entries)); | |
| break; | |
| } | |
| } | |
| _serializeClass(symbol) { | |
| const declaration = symbol.valueDeclaration; | |
| const handlers = []; | |
| const propertySymbols = this._checker | |
| .getTypeOfSymbolAtLocation(symbol, declaration) | |
| .getProperties(); | |
| const serializeParameter = this._serializeParameter.bind(this); | |
| const serializeTypeParameter = this._serializeTypeParameter.bind(this); | |
| propertySymbols.forEach(propertySymbol => { | |
| if (!this._isActionHandler(propertySymbol)) { | |
| return; | |
| } | |
| const doc = propertySymbol.getDocumentationComment(); | |
| const signature = this._checker | |
| .getTypeOfSymbolAtLocation(propertySymbol, declaration) | |
| .getCallSignatures()[0]; | |
| handlers.push({ | |
| doc: TS.displayPartsToString(propertySymbol.getDocumentationComment()), | |
| typeParameters: signature.typeParameters && | |
| signature.typeParameters.map(serializeTypeParameter), | |
| name: propertySymbol.getName(), | |
| parameters: signature.parameters.map(serializeParameter), | |
| returnType: this._checker.typeToString(signature.getReturnType()) | |
| }); | |
| }); | |
| //console.log(symbol); | |
| return { | |
| name: declaration.name.text, | |
| doc: TS.displayPartsToString(symbol.getDocumentationComment()), | |
| handlers: handlers | |
| } | |
| } | |
| _isActionModule(symbol) { | |
| const declaration = symbol.valueDeclaration | |
| // console.log(declaration.heritageClauses[0].types[0].expression.name.text); | |
| if (!declaration.heritageClauses || | |
| !declaration.heritageClauses.length || | |
| !declaration.heritageClauses.some(this._isActionHandlersClass) | |
| ) { | |
| return; | |
| } | |
| return true; | |
| } | |
| _isActionHandlersClass(target) { | |
| if (!target.types || !target.types.length) { | |
| return; | |
| } | |
| return target.types.some(type => type.expression.name.text === 'ActionHandlers'); | |
| } | |
| _isActionHandler(symbol) { | |
| const valueDeclaration = symbol.valueDeclaration; | |
| return ( | |
| valueDeclaration && | |
| (valueDeclaration.kind == TS.SyntaxKind.MethodDeclaration) && | |
| (valueDeclaration.flags & TS.NodeFlags.Static) && | |
| !(valueDeclaration.flags & TS.NodeFlags.Private) && | |
| symbol.valueDeclaration.decorators && | |
| symbol.valueDeclaration.decorators.length && | |
| symbol.valueDeclaration.decorators.some(this._isActionHandlerDecorator) | |
| ) | |
| } | |
| _isActionHandlerDecorator(decorator) { | |
| return ( | |
| decorator.expression && | |
| decorator.expression.expression && | |
| decorator.expression.expression.name.text === 'action' | |
| ); | |
| } | |
| _serializeParameter(symbol) { | |
| const declaration = symbol.valueDeclaration; | |
| const optional = !!(declaration.initializer || declaration.questionToken); | |
| const rest = !!declaration.dotDotDotToken; | |
| const type = this._checker | |
| .typeToString(this._checker.getTypeOfSymbolAtLocation(symbol, declaration)); | |
| let doc = TS.displayPartsToString(symbol.getDocumentationComment()) | |
| return { | |
| doc: doc, | |
| name: symbol.getName(), | |
| optional: optional, | |
| rest: rest, | |
| type: type | |
| }; | |
| } | |
| _serializeTypeParameter(typeParameter) { | |
| return this._checker.typeToString(typeParameter); | |
| } | |
| static entriesToString(entries) { | |
| let output = ""; | |
| for (let entrie of entries) { | |
| output += | |
| (!!entrie.doc ? ` /*${entrie.doc}*/\n` : '') | |
| + ` export namespace ${entrie.name} {\n` | |
| + entrie.handlers.map(handlerToString).join('\n') + '\n' | |
| + ' }\n\n'; | |
| } | |
| return output; | |
| function handlerToString(handler) { | |
| let parameters = []; | |
| let parameterDocs = []; | |
| for (let parameter of handler.parameters) { | |
| parameters.push( | |
| `${parameter.rest ? '...' : ''}${parameter.name}${parameter.optional ? '?' : ''}: ${parameter.type}` | |
| ); | |
| parameterDocs.push( | |
| `* @param {${parameter.optional ? 'optional ' : ''}${parameter.rest ? '...' : ''}${parameter.type}} ${parameter.name}` | |
| ); | |
| !!parameter.doc && parameterDocs.push( | |
| `* ${parameter.doc.replace(/\n/g, '\n * ')}` | |
| ); | |
| } | |
| //parameters = parameters.map(paramToString); | |
| let typeParameters = handler.typeParameters && handler.typeParameters.length ? | |
| `<${handler.typeParameters.join(',')}>` : ""; | |
| return ( | |
| ` /**\n` | |
| + ` * ${handler.doc}\n` | |
| + (parameterDocs.length ? | |
| ` ${parameterDocs.join('\n ')}\n` : '') | |
| + ` */\n` | |
| + ` export function ${handler.name}${typeParameters}(${parameters.join(',')}): ${handler.returnType};\n` | |
| ); | |
| } | |
| function paramToString(param) { | |
| console.log(param) | |
| return param; | |
| } | |
| } | |
| } | |
| class Synchronizer extends Events.EventEmitter { | |
| constructor(fileNames, options) { | |
| super(); | |
| this._options = options; | |
| this._fileNames = fileNames || []; | |
| this._cacheMap = {}; | |
| this._syncTimerId = null; | |
| this._workId = 0; | |
| this._overwriteSourceFileMap = {}; | |
| if (this._fileNames && this._fileNames.length) { | |
| this._fileNames = this._fileNames.map(fileName => { | |
| fileName = this._normalFileName(fileName); | |
| return fileName; | |
| }); | |
| } | |
| this.sync(); | |
| } | |
| addScriptFile(fileName) { | |
| fileName = this._normalFileName(fileName); | |
| this._cacheMap[fileName] = null; | |
| this._fileNames.push(fileName); | |
| this._createProgram(); | |
| this.sync(); | |
| } | |
| removeScriptFile(fileName) { | |
| fileName = this._normalFileName(fileName); | |
| delete this._cacheMap[fileName]; | |
| Utils.removeArrayItem(this._fileNames, fileName); | |
| this._createProgram(); | |
| this.sync(); | |
| } | |
| updateScriptFile(fileName) { | |
| fileName = this._normalFileName(fileName); | |
| if (!this._hit(fileName)) { | |
| return; | |
| } | |
| this._cacheMap[fileName] = null; | |
| this._createProgram(); | |
| this.sync(); | |
| // FS.readFile(fileName, "utf8", (err, data) => { | |
| // if (err) { | |
| // return; | |
| // } | |
| // this._cacheMap[fileName] = null; | |
| // let newText = data.toString(); | |
| // let sourceFile = this._program.getSourceFile(fileName); | |
| // sourceFile = sourceFile.update(newText, { | |
| // span: { | |
| // start: 0, | |
| // length: sourceFile.text.length | |
| // }, | |
| // newLength: newText.length | |
| // }) | |
| // this._overwriteSourceFileMap[fileName] = sourceFile; | |
| // this.sync(); | |
| // }); | |
| } | |
| sync() { | |
| if (this._syncTimerId) { | |
| clearTimeout(this._syncTimerId); | |
| } | |
| this._syncTimerId = setTimeout(this._sync.bind(this, ++this._workId), 300); | |
| } | |
| _sync(workId) { | |
| if (workId !== this._workId) { | |
| return; | |
| } | |
| if (!this._program) { | |
| this._createProgram(); | |
| } | |
| let output = ( | |
| 'declare module "mao-app/actions" {\n' | |
| + this._getSourceFiles().map(sourceFile => { | |
| const fileName = sourceFile.fileName; | |
| if (!this._cacheMap[fileName]) { | |
| this._cacheMap[fileName] = this._parser.parse(sourceFile); | |
| } | |
| return this._cacheMap[fileName]; | |
| }).join('\n') + '\n' | |
| + '}\n' | |
| ); | |
| this.emit('syncBefore'); | |
| FS.writeFile(OUTPUT_FILE_PATH, output, (err) => { | |
| if (err) { | |
| this.emit('syncFailed', err) | |
| return; | |
| } | |
| this.emit('syncSuccess'); | |
| }); | |
| } | |
| _createProgram() { | |
| this._program = TS.createProgram( | |
| this._fileNames, | |
| this._options.compilerOptions, | |
| null, | |
| this._program | |
| ); | |
| this._checker = this._program.getTypeChecker() | |
| this._overwriteSourceFileMap = {}; | |
| this._parser = new Parser(this._program, this._checker); | |
| } | |
| _hit(fileName) { | |
| if (!this._hitCacheMap) { | |
| this._hitCacheMap = {}; | |
| } | |
| if (this._hitCacheMap[fileName]) { | |
| return this._hitCacheMap[fileName]; | |
| } | |
| return this._hitCacheMap[fileName] = !this._options.exclude.some((pattern) => { | |
| return pattern.match(fileName); | |
| }); | |
| } | |
| _normalFileName(fileName) { | |
| return Path.resolve(this._options.projectDir, fileName); | |
| } | |
| _getSourceFiles() { | |
| const sourceFiles = []; | |
| this._fileNames.forEach(fileName => { | |
| if (!this._hit(fileName)) { | |
| return; | |
| } | |
| sourceFiles.push( | |
| //this._overwriteSourceFileMap[fileName] || | |
| this._program.getSourceFile(fileName) | |
| ); | |
| }); | |
| return sourceFiles; | |
| } | |
| } | |
| function actionsSynchronizer(appConfig) { | |
| const options = OPTIONS; | |
| const fileNames = []; | |
| const watcher = Chokidar.watch( | |
| options.include || [], | |
| { cwd: options.projectDir } | |
| ); | |
| const promise = new Promise(); | |
| let synchronizer = null; | |
| watcher.on('add', (file) => { | |
| if (!synchronizer) { | |
| fileNames.push(file); | |
| return; | |
| } else { | |
| synchronizer.addScriptFile(file); | |
| } | |
| }); | |
| watcher.on('unlink', (file) => { | |
| if (!synchronizer) { | |
| Utils.removeArrayItem(fileNames, file); | |
| } else { | |
| synchronizer.removeScriptFile(file); | |
| } | |
| }); | |
| watcher.on('change', (file) => { | |
| if (!synchronizer) { | |
| return; | |
| } | |
| synchronizer.updateScriptFile(file); | |
| }); | |
| watcher.on('ready', () => { | |
| synchronizer = new Synchronizer(fileNames, options); | |
| synchronizer.once("syncSuccess", () => { | |
| promise.resolve(); | |
| }); | |
| }); | |
| return promise; | |
| } | |
| actionsSynchronizer(); | |
| module.exports = actionsSynchronizer; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment