@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
253 lines (222 loc) • 7.56 kB
text/typescript
/* tslint:disable:no-bitwise */
import * as ts from 'typescript';
import JSONSchemaNode, {
SchemaNode,
StringSchemaNode,
ArraySchemaNode,
ObjectSchemaNode,
EnumSchemaNode,
PrimitiveSchemaNode,
RefSchemaNode,
EmptySchemaNode,
AnyOfSchemaNode,
AllOfSchemaNode,
} from './json-schema-nodes';
// Assuming that the last param will be a file, can be replaced with something like yargs in future
const file = process.argv[process.argv.length - 1];
const files = [file];
const program = ts.createProgram(files, { jsx: ts.JsxEmit.React });
const checker = program.getTypeChecker();
const typeIdToDefName: Map<number, string> = new Map();
const jsonSchema = new JSONSchemaNode(
'draft-04',
'Schema for Atlassian Editor documents.',
'doc_node'
);
let ticks = 0;
program.getSourceFiles().forEach(walk);
waitForTicks().then(() => {
/* tslint:disable-next-line:no-console */
console.log(JSON.stringify(jsonSchema));
});
// Functions
function waitForTicks() {
return new Promise(resolve => {
const waitForTick = () => {
process.nextTick(() => {
ticks--;
ticks > 0 ? waitForTick() : resolve();
});
};
waitForTick();
});
}
function walk(node: ts.Node) {
if (isSourceFile(node)) {
node.forEachChild(walk);
} else if (isInterfaceDeclaration(node) || isTypeAliasDeclaration(node)) {
const symbol: ts.Symbol = (node as any).symbol;
const { name, ...rest } = getTags(symbol.getJsDocTags());
if (name) {
if (jsonSchema.hasDefinition(name)) {
throw new Error(`Duplicate definition for ${name}`);
}
const type = checker.getTypeAtLocation(node);
jsonSchema.addDefinition(name, getSchemaNodeFromType(type, rest));
typeIdToDefName.set((type as any).id, name);
}
} else {
// If in future we need support for other nodes, this will help to debug
// console.log(syntaxKindToName(node.kind));
// node.forEachChild(walk);
}
}
function getSchemaNodeFromType(type: ts.Type, validators = {}): SchemaNode {
// Found a $ref
if (typeIdToDefName.has((type as any).id)) {
return new RefSchemaNode(
`#/definitions/${typeIdToDefName.get((type as any).id)!}`
);
} else if (isStringType(type)) {
return new StringSchemaNode(validators);
} else if (isBooleanType(type)) {
return new PrimitiveSchemaNode('boolean');
} else if (isNumberType(type)) {
return new PrimitiveSchemaNode('number', validators);
} else if (isUnionType(type)) {
const isEnum = type.types.every(t => isStringLiteralType(t));
if (isEnum) {
return new EnumSchemaNode(
type.types.map(t => (t as ts.LiteralType).text)
);
} else {
return new AnyOfSchemaNode(type.types.map(t => getSchemaNodeFromType(t)));
}
} else if (isIntersectionType(type)) {
return new AllOfSchemaNode(type.types.map(
t => getSchemaNodeFromType(t, getTags(t.getSymbol().getJsDocTags()))
));
} else if (isArrayType(type)) {
const types = type.typeArguments.length === 1 // Array< X | Y >
? [type.typeArguments[0]]
: type.typeArguments;
return new ArraySchemaNode(
types.length === 1 && isAnyType(types[0]) // Array<any>
? []
: types.map(t => getSchemaNodeFromType(t)),
validators
);
} else if (isObjectType(type)) {
const obj = new ObjectSchemaNode({}, { additionalProperties: false, ...validators });
// Use node's queue to prevent circular dependency
process.nextTick(() => {
ticks++;
const props = checker.getPropertiesOfType(type);
props.forEach(prop => {
const name = prop.getName();
// Drop private properties __fileName, __fileType, etc
if ((name[0] !== '_' || name[1] !== '_') && prop.valueDeclaration) {
const propType = checker.getTypeOfSymbolAtLocation(
prop,
prop.valueDeclaration
);
const isRequired = (prop.getFlags() & ts.SymbolFlags.Optional) === 0;
const validators = getTags(prop.getJsDocTags());
obj.addProperty(
name,
getSchemaNodeFromType(propType, validators),
isRequired
);
}
});
});
return obj;
} else if (isLiteralType(type)) {
// Using ConstSchemaNode doesn't pass validation
return new EnumSchemaNode(extractLiteralValue(type));
} else if (isNonPrimitiveType(type)) {
// object
return new EmptySchemaNode();
}
throw new Error(`TODO: ${checker.typeToString(type)} to be defined`);
}
type TagInfo = { name: string };
function getTags(tagInfo: ts.JSDocTagInfo[]): TagInfo {
return tagInfo.reduce((obj, { name, text = '' }) => {
let val: any = text;
if (/^\d+$/.test(text)) {
// Number
val = +text;
} else if (text[0] === '"') {
// " wrapped string
val = JSON.parse(text);
} else if (text === 'true') {
val = true;
} else if (text === 'false') {
val = false;
}
(obj as any)[name] = val;
return obj;
}, {} as TagInfo);
}
type PrimitiveType = number | boolean | string;
function extractLiteralValue(typ: ts.Type): PrimitiveType {
if (typ.flags & ts.TypeFlags.EnumLiteral) {
let str = (<ts.LiteralType>typ).text;
let num = parseFloat(str);
return isNaN(num) ? str : num;
} else if (typ.flags & ts.TypeFlags.StringLiteral) {
return (<ts.LiteralType>typ).text;
} else if (typ.flags & ts.TypeFlags.NumberLiteral) {
return parseFloat((<ts.LiteralType>typ).text);
} else if (typ.flags & ts.TypeFlags.BooleanLiteral) {
return (typ as any).intrinsicName === 'true';
}
throw new Error(`Couldn't parse in extractLiteralValue`);
}
// Helpers
function isSourceFile(node: ts.Node): node is ts.SourceFile {
return node.kind === ts.SyntaxKind.SourceFile;
}
function isInterfaceDeclaration(
node: ts.Node
): node is ts.InterfaceDeclaration {
return node.kind === ts.SyntaxKind.InterfaceDeclaration;
}
function isTypeAliasDeclaration(
node: ts.Node | ts.Declaration
): node is ts.TypeAliasDeclaration {
return node.kind === ts.SyntaxKind.TypeAliasDeclaration;
}
function isStringType(type: ts.Type) {
return (type.flags & ts.TypeFlags.String) > 0;
}
function isBooleanType(type: ts.Type) {
return (type.flags & ts.TypeFlags.Boolean) > 0;
}
function isNumberType(type: ts.Type) {
return (type.flags & ts.TypeFlags.Number) > 0;
}
function isUnionType(type: ts.Type): type is ts.UnionType {
return (type.flags & ts.TypeFlags.Union) > 0;
}
function isIntersectionType(type: ts.Type): type is ts.IntersectionType {
return (type.flags & ts.TypeFlags.Intersection) > 0;
}
function isArrayType(type: ts.Type): type is ts.TypeReference {
return (
(type.flags & ts.TypeFlags.Object) > 0 &&
((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) > 0 &&
type.getSymbol().getName() === 'Array'
);
}
function isObjectType(type: ts.Type): type is ts.ObjectType {
return (type.flags & ts.TypeFlags.Object) > 0;
}
function isStringLiteralType(type: ts.Type): type is ts.LiteralType {
return (type.flags & ts.TypeFlags.StringLiteral) > 0;
}
function isLiteralType(type: ts.Type): type is ts.LiteralType {
return (type.flags & ts.TypeFlags.Literal) > 0;
}
function isNonPrimitiveType(type: ts.Type): type is ts.LiteralType {
return (type.flags & ts.TypeFlags.NonPrimitive) > 0;
}
function isAnyType(type: ts.Type): type is ts.Type {
return (type.flags & ts.TypeFlags.Any) > 0;
}
/*
function syntaxKindToName(kind: ts.SyntaxKind) {
return ts.SyntaxKind[kind];
}
*/