Skip to content

Instantly share code, notes, and snippets.

@maolion
Created September 28, 2016 08:07
Show Gist options
  • Select an option

  • Save maolion/5fee8364df06eecdc61fe8076f588ddc to your computer and use it in GitHub Desktop.

Select an option

Save maolion/5fee8364df06eecdc61fe8076f588ddc to your computer and use it in GitHub Desktop.
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