├── packages ├── core │ ├── types │ │ ├── index.ts │ │ ├── schema.type.ts │ │ ├── any.type.ts │ │ ├── ref.type.ts │ │ ├── string.type.ts │ │ ├── big-int.type.ts │ │ ├── number.type.ts │ │ ├── integer.type.ts │ │ ├── float.type.ts │ │ ├── double.type.ts │ │ ├── boolean.type.ts │ │ ├── enum.type.ts │ │ ├── array.type.ts │ │ ├── date.type.ts │ │ ├── type.ts │ │ └── object.type.ts │ ├── errors │ │ ├── index.ts │ │ ├── runtime-error.ts │ │ └── schema-error.ts │ ├── decorators │ │ ├── index.ts │ │ ├── type.decorator.ts │ │ └── schema.decorator.ts │ ├── utils │ │ ├── index.ts │ │ ├── uuid.ts │ │ ├── global.ts │ │ └── type-check.ts │ ├── interfaces │ │ ├── reflection.interface.ts │ │ ├── param-info.interface.ts │ │ ├── index.ts │ │ ├── enum-info.interface.ts │ │ ├── type.interface.ts │ │ ├── enum.interface.ts │ │ ├── schema │ │ │ ├── schema-visitor.interface.ts │ │ │ ├── type.interface.ts │ │ │ └── json-schema.interface.ts │ │ └── special-types.interface.ts │ ├── package-lock.json │ ├── enums │ │ ├── ts-modifier.enum.ts │ │ ├── index.ts │ │ ├── type-id.enum.ts │ │ ├── number-formats.enum.ts │ │ ├── data-type.enum.ts │ │ └── string-formats.enum.ts │ ├── tsconfig.json │ ├── index.ts │ ├── primitive-types.ts │ ├── constants.ts │ ├── package.json │ ├── schema │ │ ├── index.ts │ │ ├── schema-definition.ts │ │ ├── schema-visitor.ts │ │ └── schema.ts │ ├── special-types.ts │ └── type-factory.ts ├── compiler │ ├── transformers │ │ ├── index.ts │ │ └── easy.transformer.ts │ ├── index.ts │ ├── interfaces │ │ ├── index.ts │ │ ├── translator.interface.ts │ │ └── reflect-context.interface.ts │ ├── tsconfig.json │ ├── utils │ │ ├── index.ts │ │ ├── ts-node-factory.ts │ │ ├── comment-parser.ts │ │ ├── ts-utils.ts │ │ └── type-parser.ts │ ├── register │ │ ├── index.ts │ │ ├── type-check.ts │ │ └── transpile-only.ts │ ├── translators │ │ ├── index.ts │ │ ├── import.translator.ts │ │ ├── enum.translator.ts │ │ └── class.translator.ts │ ├── package.json │ ├── transformer-tester.ts │ ├── constants.ts │ └── ts-node │ │ └── index.ts └── cli │ ├── tsconfig.json │ ├── help.ts │ ├── package.json │ ├── easy.ts │ ├── package-lock.json │ ├── gulpfile.ts │ └── commands │ ├── build.ts │ └── start.ts ├── .gitignore ├── tsconfig.json ├── tslint.json ├── package.json ├── gulpfile.js └── README.MD /packages/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './type'; 2 | -------------------------------------------------------------------------------- /packages/core/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runtime-error'; 2 | export * from './schema-error'; 3 | -------------------------------------------------------------------------------- /packages/core/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './schema.decorator'; 2 | export * from './type.decorator'; 3 | -------------------------------------------------------------------------------- /packages/compiler/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './easy.transformer'; 2 | export * from '../transformer-tester'; 3 | -------------------------------------------------------------------------------- /packages/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './global'; 2 | export * from './type-check'; 3 | export * from './uuid'; 4 | -------------------------------------------------------------------------------- /packages/core/interfaces/reflection.interface.ts: -------------------------------------------------------------------------------- 1 | interface IReflectable { 2 | getSchema(); 3 | 4 | getypeInfo(); 5 | }; -------------------------------------------------------------------------------- /packages/compiler/index.ts: -------------------------------------------------------------------------------- 1 | export * from './register'; 2 | export * from './transformers'; 3 | export * from './translators'; 4 | -------------------------------------------------------------------------------- /packages/compiler/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reflect-context.interface'; 2 | export * from './translator.interface'; 3 | -------------------------------------------------------------------------------- /packages/core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easytype/core", 3 | "version": "0.0.98", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/enums/ts-modifier.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TsModifier { 2 | Public = 1, 3 | Private = 2, 4 | Protected = 3, 5 | Static = 4, 6 | Async = 5 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/errors/runtime-error.ts: -------------------------------------------------------------------------------- 1 | export class RuntimeError { 2 | constructor(private message: string) { 3 | this.message = 'EasyType Runtime Error:' + this.message; 4 | } 5 | } -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": [ 7 | "*.ts", 8 | "**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./" 5 | }, 6 | "include": [ 7 | "*.ts", 8 | "**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/compiler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | }, 6 | "include": [ 7 | "*.ts", 8 | "**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /packages/compiler/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comment-parser'; 2 | export * from './generate-code'; 3 | export * from './ts-node-factory'; 4 | export * from './ts-utils'; 5 | export * from './type-parser'; 6 | -------------------------------------------------------------------------------- /packages/compiler/register/index.ts: -------------------------------------------------------------------------------- 1 | import * as TsNode from '../ts-node'; 2 | import { EasyTransformer } from '../transformers'; 3 | 4 | TsNode.register({ 5 | transformers: { before: [EasyTransformer()] } 6 | }); 7 | -------------------------------------------------------------------------------- /packages/core/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-type.enum'; 2 | export * from './type-id.enum'; 3 | export * from './number-formats.enum'; 4 | export * from './string-formats.enum'; 5 | export * from './ts-modifier.enum'; 6 | -------------------------------------------------------------------------------- /packages/compiler/interfaces/translator.interface.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Undefinable } from '@easytype/core'; 3 | 4 | export interface ITranslator { 5 | (node: ts.Node): Undefinable; 6 | } -------------------------------------------------------------------------------- /packages/compiler/register/type-check.ts: -------------------------------------------------------------------------------- 1 | import * as TsNode from '../ts-node'; 2 | import { EasyTransformer } from '../transformers'; 3 | 4 | TsNode.register({ 5 | transformers: { before: [EasyTransformer()] }, 6 | typeCheck: true 7 | }); 8 | -------------------------------------------------------------------------------- /packages/compiler/interfaces/reflect-context.interface.ts: -------------------------------------------------------------------------------- 1 | import { GenericArg, Nullable } from '@easytype/core'; 2 | 3 | export interface ReflectContext { 4 | reflectable: boolean; 5 | props: string[]; 6 | args: Nullable; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/enums/type-id.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TypeId { 2 | OBJECT_TYPE = 1, 3 | PROP_TYPE = 2, 4 | GENERIC_TYPE = 3, 5 | GENERIC_VARIABLE_TYPE = 4, 6 | TYPE_OF_TYPE = 5, 7 | UNION_TYPE = 6, 8 | INTERSECTION_TYPE = 7, 9 | FUNCTION 10 | } -------------------------------------------------------------------------------- /packages/core/enums/number-formats.enum.ts: -------------------------------------------------------------------------------- 1 | export enum NumberFormats { 2 | FLOAT = 'float', 3 | DOUBLE = 'double', 4 | INTEGER = 'int32', 5 | BIG_INT = 'int64', 6 | 7 | TIME = 'time', 8 | DATE = 'date', 9 | DATE_TIME = 'date-time', 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/interfaces/param-info.interface.ts: -------------------------------------------------------------------------------- 1 | import { TypeReference } from './type.interface'; 2 | 3 | export interface ParameterInfo { 4 | decorators: Function[]; 5 | 6 | name: string; 7 | 8 | type: TypeReference; 9 | 10 | required?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/errors/schema-error.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema } from 'interfaces'; 2 | 3 | export class SchemaError { 4 | constructor(public schema: BaseSchema, public message: string) { 5 | this.message = `Schema '${schema.$id}' Error: ` + message; 6 | console.error(this.message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/compiler/translators/index.ts: -------------------------------------------------------------------------------- 1 | import { ClassTranslator } from './class.translator'; 2 | import { EnumTranslator } from './enum.translator'; 3 | import { ImportTranslator } from './import.translator'; 4 | 5 | export const Translators = [ 6 | ImportTranslator, 7 | EnumTranslator, 8 | ClassTranslator, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './decorators'; 3 | export * from './enums'; 4 | export * from './special-types'; 5 | export * from './interfaces'; 6 | export * from './primitive-types'; 7 | export * from './schema'; 8 | export * from './type-factory'; 9 | export * from './types'; 10 | export * from './utils'; 11 | -------------------------------------------------------------------------------- /packages/core/primitive-types.ts: -------------------------------------------------------------------------------- 1 | export class Float extends Number { } 2 | 3 | export class Double extends Number { } 4 | 5 | export class Integer extends Number { } 6 | 7 | export class BigInt extends Number { } 8 | 9 | export class Any { } 10 | 11 | export class Null { } 12 | 13 | export class ObjectLiteral { 14 | [key: string]: T; 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const PARAMTYPES_METADATA = 'design:paramtypes'; 2 | 3 | export const TYPE_METADATA = 'design:type'; 4 | 5 | export const EASY_METADATA = 'easy:metadata'; 6 | 7 | export const SCHEMA_METADATA = 'schema:metadata'; 8 | 9 | export const SYMBOL_TYPE_KEY = 'x:type'; 10 | 11 | export const TYPE_SYMBOL = Symbol.for(SYMBOL_TYPE_KEY); 12 | -------------------------------------------------------------------------------- /packages/core/types/schema.type.ts: -------------------------------------------------------------------------------- 1 | import { IType, JSONSchema } from '../interfaces'; 2 | import { DataType } from '../enums'; 3 | 4 | export function SchemaType(schema: Partial = {}): IType { 5 | return { 6 | type: DataType.SCHEMA, 7 | schema: () => { 8 | return schema as JSONSchema; 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enum-info.interface'; 2 | export * from './enum.interface'; 3 | export * from './param-info.interface'; 4 | export * from './schema/json-schema.interface'; 5 | export * from './schema/type.interface'; 6 | export * from './schema/schema-visitor.interface'; 7 | export * from './special-types.interface'; 8 | export * from './type.interface'; 9 | -------------------------------------------------------------------------------- /packages/core/decorators/type.decorator.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { TypeCtor } from '../interfaces'; 3 | import { Type } from '../types'; 4 | 5 | /** 6 | * Define new type 7 | */ 8 | export function TypeDefine(id: string, type: TypeCtor): ClassDecorator { 9 | return (target: any) => { 10 | DataType[id] = id; 11 | Type.set(id, type); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | export function uuid(size: number) { 2 | const data = new Array(size); 3 | for (let i = 0; i < size; ++i) { 4 | data[i] = Math.random() * 0xff | 0; 5 | } 6 | let result = ''; 7 | for (let offset = 0; offset < size; ++offset) { 8 | const byte = data[offset]; 9 | result += byte.toString(16).toLowerCase(); 10 | } 11 | return result; 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # tests 5 | /coverage 6 | /.nyc_output 7 | 8 | # build 9 | build/ 10 | 11 | # IDE 12 | /.idea 13 | /.awcache 14 | /.vscode 15 | 16 | # bundle 17 | packages/**/*.d.ts 18 | packages/**/*.js 19 | 20 | # misc 21 | .DS_Store 22 | lerna-debug.log 23 | npm-debug.log 24 | yarn-error.log 25 | /**/npm-debug.log 26 | /packages/**/.npmignore 27 | /packages/**/LICENSE 28 | 29 | /packages/**/test/ 30 | -------------------------------------------------------------------------------- /packages/compiler/register/transpile-only.ts: -------------------------------------------------------------------------------- 1 | import * as TsNode from '../ts-node'; 2 | import { EasyTransformer } from '../transformers'; 3 | 4 | TsNode.register({ 5 | compilerOptions: { 6 | module: 'commonjs', 7 | experimentalDecorators: true, 8 | emitDecoratorMetadata: true, 9 | target: 'es2017' 10 | }, 11 | transformers: { before: [EasyTransformer()] }, 12 | transpileOnly: true 13 | }); 14 | -------------------------------------------------------------------------------- /packages/compiler/utils/ts-node-factory.ts: -------------------------------------------------------------------------------- 1 | import { generateFactoryCode } from './generate-code'; 2 | import * as ts from 'typescript'; 3 | import { tsquery } from '@phenomnomnominal/tsquery'; 4 | 5 | export class TSNodeFactory { 6 | public static generateAst(source: string): ts.Node { 7 | const node: ts.Node = tsquery.ast(source); 8 | const fun = new Function('ts', `return ${generateFactoryCode(node)}`); 9 | return fun(ts); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/interfaces/enum-info.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 枚举字段声明 3 | */ 4 | export interface EnumField { 5 | /** 6 | * 字段名 7 | */ 8 | key: string; 9 | 10 | /** 11 | * 常量值 12 | */ 13 | value: number | string; 14 | 15 | /** 16 | * 字段描述 17 | */ 18 | description: string; 19 | } 20 | 21 | /** 22 | * enum info 23 | */ 24 | export interface EnumInfo { 25 | name: string; 26 | 27 | description: string; 28 | 29 | fields: EnumField[]; 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easytype/core", 3 | "version": "0.0.98", 4 | "description": "Runtime type system based on typescript.", 5 | "author": "davan.c", 6 | "license": "ISC", 7 | "dependencies": { 8 | "reflect-metadata": "^0.1.13", 9 | "moment": "^2.24.0", 10 | "lodash": "^4.17.15" 11 | }, 12 | "devDependencies": { 13 | "@types/mocha": "^5.2.7", 14 | "@types/node": "^12.7.8", 15 | "@types/lodash": "^4.14.142" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/enums/data-type.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础数据类型 3 | */ 4 | export enum DataType { 5 | NUMBER = 'number', 6 | STRING = 'string', 7 | BOOLEAN = 'boolean', 8 | ARRAY = 'array', 9 | OBJECT = 'object', 10 | 11 | FLOAT = 'float', 12 | DOUBLE = 'double', 13 | INTEGER = 'int32', 14 | BIG_INT = 'int64', 15 | DATE_TIME = 'date-time', 16 | 17 | BUFFER = 'buffer', 18 | ENUM = 'enum', 19 | 20 | NULL = 'null', 21 | ANY = 'any', 22 | 23 | REFERENCE = 'ref', 24 | SCHEMA = 'schema', 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/types/any.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { AnySchema, IType } from '../interfaces'; 3 | 4 | export function AnyType(schema: any = {}): IType { 5 | // schema.type = [ 6 | // DataType.NUMBER, 7 | // DataType.STRING, 8 | // DataType.BOOLEAN, 9 | // DataType.OBJECT, 10 | // DataType.ARRAY, 11 | // DataType.NULL 12 | // ]; 13 | schema.$type = DataType.ANY; 14 | 15 | return { 16 | type: DataType.ANY, 17 | schema: () => schema as AnySchema, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/interfaces/type.interface.ts: -------------------------------------------------------------------------------- 1 | export type Fields = keyof T; 2 | 3 | export type Nullable = T | null; 4 | 5 | export type Undefinable = T | undefined; 6 | 7 | /** 8 | * Partial PLUS ++ 9 | */ 10 | export type AnyOf = { 11 | [P in keyof T]?: V; 12 | }; 13 | 14 | export interface Abstract { 15 | prototype: T; 16 | } 17 | 18 | export type Class = { 19 | new(...args: any[]): T; 20 | }; 21 | 22 | export type TypeReference = Function | any[]; 23 | 24 | export type Inherits = Partial>; 25 | -------------------------------------------------------------------------------- /packages/core/types/ref.type.ts: -------------------------------------------------------------------------------- 1 | import { RefSchema, IType, JSONSchema } from '../interfaces'; 2 | import { DataType } from '../enums'; 3 | 4 | export function RefType(id: string, root: JSONSchema, ref: JSONSchema): IType { 5 | if (root) { 6 | root.$defs = root.$defs || {}; 7 | root.$defs[id] = ref; 8 | } 9 | const schema = { 10 | $ref: `#/$defs/${id}`, 11 | type: undefined, 12 | $type: undefined, 13 | }; 14 | 15 | return { 16 | type: DataType.REFERENCE, 17 | schema: () => schema as RefSchema, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/types/string.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { IType, StringSchema } from '../interfaces'; 3 | 4 | export function StringType(schema: Partial = {}): IType { 5 | schema.type = schema.type || DataType.STRING; 6 | 7 | return { 8 | type: DataType.STRING, 9 | schema: () => { 10 | return schema as StringSchema; 11 | }, 12 | is: (value: any) => { 13 | return value instanceof String || typeof value === 'string'; 14 | }, 15 | value: (value: any) => { 16 | return value ? value.toString() : undefined; 17 | } 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "skipLibCheck": true, 7 | "suppressImplicitAnyIndexErrors": true, 8 | "noUnusedLocals": false, 9 | "removeComments": true, 10 | "noLib": false, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es2017", 14 | "sourceMap": true, 15 | "allowJs": false, 16 | "strict": true, 17 | "strictNullChecks": false, 18 | "strictPropertyInitialization": false, 19 | "outDir": "./build", 20 | "resolveJsonModule": true 21 | }, 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } -------------------------------------------------------------------------------- /packages/cli/help.ts: -------------------------------------------------------------------------------- 1 | export interface ModuleInfo { 2 | name: string; 3 | path: string; 4 | require: boolean; 5 | } 6 | 7 | export function resolveModules(modules: ModuleInfo[], cb?: (module: ModuleInfo) => void) { 8 | for (const module of modules) { 9 | try { 10 | module.path = require.resolve(module.path); 11 | if (cb) { 12 | cb(module); 13 | } 14 | } catch (err) { 15 | if (module.require) { 16 | console.log(`ERROR: '${module.name}' module is not installed, please install it first ($npm i ${module.name} --save-dev)`); 17 | process.exit(0); 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /packages/core/types/big-int.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { BigIntSchema, IType } from '../interfaces'; 3 | 4 | export function BigIntType(schema: Partial = {}): IType { 5 | schema.type = schema.type || DataType.NUMBER; 6 | schema.format = schema.format || DataType.BIG_INT; 7 | schema.$type = DataType.BIG_INT; 8 | 9 | return { 10 | type: DataType.BIG_INT, 11 | schema: () => { 12 | return schema as BigIntSchema; 13 | }, 14 | is: (value: any) => { 15 | return value instanceof BigInt; 16 | }, 17 | value: (value: any) => { 18 | return BigInt(value); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/interfaces/enum.interface.ts: -------------------------------------------------------------------------------- 1 | import { EnumInfo } from './enum-info.interface'; 2 | import { Undefinable } from './type.interface'; 3 | 4 | export interface EnumInterface { 5 | readonly keys: string[]; 6 | 7 | readonly values: number[] | string[]; 8 | 9 | getValue(key: string): Undefinable; 10 | 11 | hasValue(value: any): boolean; 12 | 13 | getKeys(value: any): string[]; 14 | 15 | getKey(value: any): Undefinable; 16 | 17 | hasKey(key: string): boolean; 18 | 19 | getDescription(key: string): Undefinable; 20 | } 21 | 22 | export type Enum = 23 | { readonly [P in keyof T]: T[P]; } 24 | & Readonly 25 | & EnumInterface 26 | ; 27 | -------------------------------------------------------------------------------- /packages/core/types/number.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { NumberSchema, IType } from '../interfaces'; 3 | import { isNumber } from '../utils'; 4 | 5 | export function NumberType(schema: Partial = {}): IType { 6 | schema.type = schema.type || DataType.NUMBER; 7 | 8 | return { 9 | type: DataType.NUMBER, 10 | schema: () => { 11 | return schema as NumberSchema; 12 | }, 13 | is: (value: any) => { 14 | return isNumber(value) && !isNaN(value); 15 | }, 16 | value: (value: any) => { 17 | value = isNumber(value) ? value : Number(value); 18 | return !isNaN(value) ? value : undefined; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easytype/cli", 3 | "version": "0.0.13", 4 | "description": "Runtime type system based on typescript.", 5 | "main": "easy.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "davan.c", 10 | "license": "ISC", 11 | "bin": { 12 | "easy": "easy.js" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^12.7.8", 16 | "@types/yargs": "^15.0.4" 17 | }, 18 | "dependencies": { 19 | "inquirer": "^6.3.1", 20 | "gulp": "^4.0.2", 21 | "gulp-typescript": "^5.0.1", 22 | "merge2": "^1.3.0", 23 | "yargs": "^15.3.0" 24 | }, 25 | "peerDependencies": { 26 | "@easytype/compiler": "^0.0.53" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/interfaces/schema/schema-visitor.interface.ts: -------------------------------------------------------------------------------- 1 | import { ArraySchema, ObjectSchema, RefSchema, JSONSchema } from './json-schema.interface'; 2 | 3 | export interface ISchemaVisitor { 4 | visit(schema: JSONSchema, context?: TContext): TContext; 5 | 6 | visitField(field: string, schema: JSONSchema, context?: TContext): TContext; 7 | 8 | visitRef(field: string, schema: RefSchema, context?: TContext): TContext; 9 | 10 | visitAllOf(field: string, schema: JSONSchema, context?: TContext); 11 | 12 | visitOneOf(field: string, schema: JSONSchema, context?: TContext); 13 | 14 | visitObject(field: string, schema: ObjectSchema, context?: TContext): TContext; 15 | 16 | visitArray(field: string, schema: ArraySchema, context?: TContext): TContext; 17 | } 18 | -------------------------------------------------------------------------------- /packages/compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easytype/compiler", 3 | "version": "0.0.63", 4 | "description": "Runtime type system based on typescript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "gulp compiler:build" 8 | }, 9 | "author": "davan.c", 10 | "license": "ISC", 11 | "dependencies": { 12 | "typescript": "^3.8.3", 13 | "code-block-writer": "^10.0.0", 14 | "chalk": "^3.0.0", 15 | "comment-parser": "^0.7.5", 16 | "@phenomnomnominal/tsquery": "^4.0.0", 17 | "arg": "^4.1.0", 18 | "diff": "^4.0.1", 19 | "make-error": "^1.1.1", 20 | "source-map-support": "^0.5.13", 21 | "yn": "3.1.1" 22 | }, 23 | "devDependencies": {}, 24 | "peerDependencies": { 25 | "@easytype/core": "^0.0.96" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from './schema'; 2 | import { Type } from '../types'; 3 | import { Integer, BigInt, Float, Double, Any, Null } from '../primitive-types'; 4 | 5 | export * from './schema'; 6 | export * from './schema-visitor'; 7 | 8 | (() => { 9 | Schema.addRef(Number, Type.Number().schema()); 10 | Schema.addRef(String, Type.String().schema()); 11 | Schema.addRef(Boolean, Type.Boolean().schema()); 12 | Schema.addRef(Date, Type.Date().schema()); 13 | Schema.addRef(Integer, Type.Integer().schema()); 14 | Schema.addRef(BigInt, Type.BigInt().schema()); 15 | Schema.addRef(Float, Type.Float().schema()); 16 | Schema.addRef(Double, Type.Float().schema()); 17 | Schema.addRef(Any, Type.Any().schema()); 18 | Schema.addRef(Null, Type.Any().schema()); 19 | })(); 20 | -------------------------------------------------------------------------------- /packages/compiler/transformer-tester.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export class TransformerTester { 4 | public static run(source: string, transformer: () => ts.TransformerFactory) { 5 | const result = ts.transpileModule(source, { 6 | compilerOptions: { 7 | module: ts.ModuleKind.CommonJS, 8 | experimentalDecorators: true, 9 | emitDecoratorMetadata: true, 10 | // noUnusedLocals: false, 11 | // noUnusedParameters: false, 12 | target: ts.ScriptTarget.ES2017, 13 | 14 | isolatedModules: false 15 | }, 16 | transformers: { before: [transformer()] }, 17 | reportDiagnostics: true 18 | }); 19 | 20 | return result.outputText; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/compiler/transformers/easy.transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { Translators } from '../translators'; 3 | import * as pkg from '../package.json'; 4 | import * as chalk from 'chalk'; 5 | 6 | export function EasyTransformer(): ts.TransformerFactory { 7 | console.log(chalk.blue(`EasyType Transformer: v${pkg.version}`)); 8 | 9 | return context => { 10 | const visit: ts.Visitor = node => { 11 | for (const translator of Translators) { 12 | const result = translator(node); 13 | if (result) { 14 | node = result as any; 15 | break; 16 | // return result; 17 | } 18 | } 19 | 20 | return ts.visitEachChild(node, child => visit(child), context); 21 | }; 22 | 23 | return (node) => ts.visitNode(node, visit); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/types/integer.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { IntegerSchema, IType } from '../interfaces'; 3 | import { isNumber } from '../utils'; 4 | 5 | export function IntegerType(schema: Partial = {}): IType { 6 | schema.type = schema.type || DataType.NUMBER; 7 | schema.format = schema.format || DataType.INTEGER; 8 | schema.$type = DataType.INTEGER; 9 | schema.multipleOf = 1.0; 10 | 11 | return { 12 | type: DataType.INTEGER, 13 | schema: () => { 14 | return schema as IntegerSchema; 15 | }, 16 | is: (value: any) => { 17 | return isNumber(value) 18 | && !isNaN(value) 19 | && Number.isInteger(value); 20 | }, 21 | value: (value: any) => { 22 | value = parseInt(value, 0); 23 | return !Number.isNaN(value) ? value : undefined; 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/types/float.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { FloatSchema, NumberSchema, IType } from '../interfaces'; 3 | import { isNumber } from '../utils'; 4 | 5 | export function FloatType(schema: Partial = {}): IType { 6 | schema.type = schema.type || DataType.NUMBER; 7 | schema.format = schema.format || DataType.FLOAT; 8 | schema.$type = DataType.FLOAT; 9 | schema.not = { multipleOf: 1 } as NumberSchema; 10 | 11 | return { 12 | type: DataType.FLOAT, 13 | schema: () => { 14 | return schema as FloatSchema; 15 | }, 16 | is: (value: any) => { 17 | return isNumber(value) 18 | && !isNaN(value) 19 | && (value % 1 !== 0); 20 | }, 21 | value: (value: any) => { 22 | value = parseFloat(value); 23 | return !Number.isNaN(value) ? value : undefined; 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/easy.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as yargs from 'yargs'; 3 | import build from './commands/build'; 4 | import start from './commands/start'; 5 | 6 | process.on('uncaughtException', function (err) { 7 | console.log('Caught exception: ', err); 8 | }); 9 | 10 | process.on('unhandledRejection', (reason, p) => { 11 | console.log('Unhandled Rejection at:', p, 'reason:', reason); 12 | // application specific logging, throwing an error, or other logic here 13 | }); 14 | 15 | const cmd = 'easy'; 16 | 17 | yargs 18 | .option('debug', { 19 | alias: 'd', 20 | describe: 'debug', 21 | type: 'boolean' 22 | }) 23 | .usage(`Usage: ${cmd} [options]`) 24 | .command(start) 25 | .command(build) 26 | .example(`${cmd} start main.js`, 'startup project') 27 | .demandCommand(1, 'ERROR: You need at least one command before moving on') 28 | .help() 29 | .epilog('EasyType CLI Tools') 30 | .argv; -------------------------------------------------------------------------------- /packages/core/enums/string-formats.enum.ts: -------------------------------------------------------------------------------- 1 | export enum StringFormats { 2 | RELATIVE_JSON_POINTER = 'relative-json-pointer', 3 | JSON_POINTER = 'json-pointer', 4 | JSON = 'json', 5 | UUID = 'uuid', 6 | REGEX = 'regex', 7 | IPV6 = 'ipv6', 8 | IPV4 = 'ipv4', 9 | HOSTNAME = 'hostname', 10 | EMAIL = 'email', 11 | URL = 'url', 12 | URI_TEMPLATE = 'uri-template', 13 | URI_REFERENCE = 'uri-reference', 14 | URI = 'uri', 15 | TIME = 'time', 16 | DATE = 'date', 17 | DATE_TIME = 'date-time', 18 | FILE = 'file', 19 | FILES = 'files', 20 | IMAGE = 'image', 21 | IMAGES = 'images', 22 | PASSWORD = 'password', 23 | BINARY = 'binary', 24 | HEX_COLOR = 'hex-color', 25 | BASE64 = 'base64', 26 | CURRENCY = 'currency', 27 | FQDN = 'FQDN', 28 | PHONE_NUMBER = 'phone-number', 29 | ALPHA = 'alpha', 30 | ALPHANUMERIC = 'alpha-numeric', 31 | MULTI_LINE = 'multi-line', 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/types/double.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { DoubleSchema, NumberSchema, IType } from '../interfaces'; 3 | import { isNumber } from '../utils'; 4 | 5 | export function DoubleType(schema: Partial = {}): IType { 6 | schema.type = schema.type || DataType.NUMBER; 7 | schema.format = schema.format || DataType.DOUBLE; 8 | schema.$type = DataType.DOUBLE; 9 | schema.not = { multipleOf: 1 } as NumberSchema; 10 | 11 | return { 12 | type: DataType.DOUBLE, 13 | schema: () => { 14 | return schema as DoubleSchema; 15 | }, 16 | is: (value: any) => { 17 | return isNumber(value) 18 | && !isNaN(value) 19 | && (value % 1 !== 0); 20 | }, 21 | value: (value: any) => { 22 | value = parseFloat(value); 23 | return !Number.isNaN(value) ? value : undefined; 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@easytype/cli", 3 | "version": "0.0.13", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/yargs": { 8 | "version": "15.0.4", 9 | "resolved": "https://registry.npm.taobao.org/@types/yargs/download/@types/yargs-15.0.4.tgz?cache=0&sync_timestamp=1582664215643&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fyargs%2Fdownload%2F%40types%2Fyargs-15.0.4.tgz", 10 | "integrity": "sha1-fl0PjKJenVhJ8upEPPfEAt7Ngpk=", 11 | "dev": true, 12 | "requires": { 13 | "@types/yargs-parser": "*" 14 | } 15 | }, 16 | "@types/yargs-parser": { 17 | "version": "15.0.0", 18 | "resolved": "https://registry.npm.taobao.org/@types/yargs-parser/download/@types/yargs-parser-15.0.0.tgz", 19 | "integrity": "sha1-yz+fdBhp4gzOMw/765JxWQSDiC0=", 20 | "dev": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/utils/global.ts: -------------------------------------------------------------------------------- 1 | const root = typeof global === 'object' ? global : 2 | typeof self === 'object' ? self : 3 | typeof this === 'object' ? this : 4 | Function('return this;')(); 5 | 6 | export class GlobalFactory { 7 | public static createValue(name: string, value: () => T): T { 8 | name = `@@${name}`; 9 | if (typeof root[name] === 'undefined') { 10 | root[name] = value(); 11 | } 12 | return root[name]; 13 | } 14 | 15 | public static createMap(name: string): Map { 16 | return GlobalFactory.createValue(name, () => new Map()); 17 | } 18 | 19 | public static createArray(name: string): T[] { 20 | return GlobalFactory.createValue(name, () => []); 21 | } 22 | } 23 | 24 | export const createGlobalValue = GlobalFactory.createValue; 25 | 26 | export const createGlobalMap = GlobalFactory.createMap; 27 | 28 | export const createGlobalArray = GlobalFactory.createArray; 29 | -------------------------------------------------------------------------------- /packages/core/types/boolean.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { BooleanSchema, IType } from '../interfaces'; 3 | import { isBoolean, isEmpty, isString } from '../utils'; 4 | 5 | export function BooleanType(schema: Partial = {}): IType { 6 | schema.type = schema.type || DataType.BOOLEAN; 7 | 8 | return { 9 | type: DataType.BOOLEAN, 10 | schema: () => schema as BooleanSchema, 11 | is: (value: any) => { 12 | return value instanceof Boolean || typeof value === 'boolean'; 13 | }, 14 | value: (value: any) => { 15 | if (isBoolean(value) || value instanceof Boolean) { 16 | return value.valueOf(); 17 | } 18 | if (isString(value) && !isEmpty(value)) { 19 | switch (value.toLowerCase()) { 20 | case 'true': 21 | case 'yes': 22 | return true; 23 | case 'false': 24 | case 'no': 25 | return false; 26 | } 27 | } 28 | return; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "no-namespace": false, 11 | "no-empty": false, 12 | "forin": false, 13 | "no-string-literal": false, 14 | "quotemark": [ 15 | true, 16 | "single" 17 | ], 18 | "member-access": [ 19 | false 20 | ], 21 | "ordered-imports": [ 22 | false 23 | ], 24 | "max-line-length": false, 25 | "member-ordering": [ 26 | false 27 | ], 28 | "interface-name": false, 29 | "interface-over-type-literal": false, 30 | "arrow-parens": false, 31 | "object-literal-sort-keys": false, 32 | "variable-name": false, 33 | "trailing-comma": false, 34 | "max-classes-per-file": false, 35 | "no-bitwise": false, 36 | "ban-types": false, 37 | "callable-types": false, 38 | "no-console": false, 39 | "space-before-function-paren": false, 40 | "no-unused-expression": false, 41 | "prefer-for-of": false, 42 | "only-arrow-functions": false, 43 | "array-type": false 44 | }, 45 | "rulesDirectory": [] 46 | } -------------------------------------------------------------------------------- /packages/core/special-types.ts: -------------------------------------------------------------------------------- 1 | import { TypeId } from './enums'; 2 | import { GenericType, GenericTypeArg, GenericVariable, IntersectionType, TypeOfType, TypeReference, UnionType } from './interfaces'; 3 | 4 | export function $T(target: TypeReference, args: GenericTypeArg, description?: string): GenericType { 5 | return { 6 | $type: TypeId.GENERIC_TYPE, 7 | $target: target, 8 | $args: args, 9 | $description: description 10 | }; 11 | } 12 | 13 | /** 14 | * CLASS | var:T 15 | */ 16 | export function $V(name: string): GenericVariable { 17 | return { 18 | $type: TypeId.GENERIC_VARIABLE_TYPE, 19 | $name: name 20 | }; 21 | } 22 | 23 | export function $TypeOf(value: any): TypeOfType { 24 | return { 25 | $type: TypeId.TYPE_OF_TYPE, 26 | $value: value 27 | }; 28 | } 29 | 30 | export function $U(...refs: TypeReference[]): UnionType { 31 | return { 32 | $type: TypeId.UNION_TYPE, 33 | $refs: refs 34 | }; 35 | } 36 | 37 | export function $I(...refs: TypeReference[]): IntersectionType { 38 | return { 39 | $type: TypeId.INTERSECTION_TYPE, 40 | $refs: refs 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/interfaces/schema/type.interface.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral } from '../../primitive-types'; 2 | import { Fields, Undefinable } from '../type.interface'; 3 | import { JSONSchema, ObjectSchema, ArraySchema } from './json-schema.interface'; 4 | 5 | export interface IType { 6 | readonly type: string; 7 | is?: (value: any) => boolean; 8 | value?: (value: any) => Undefinable; 9 | schema: () => S; 10 | } 11 | 12 | export interface IObjectType extends IType { 13 | setId(id: string): void; 14 | description(text: string): void; 15 | prop(name: Fields, propSchema: Partial): void; 16 | required(name: string | string[]): void; 17 | optional(name: string | string[]): void; 18 | } 19 | 20 | export interface IArrayType extends IType { 21 | items(values: IType): IArrayType; 22 | contains(value: IType | IType[]): IArrayType; 23 | } 24 | 25 | // export interface IFunctionType extends IType { 26 | // parameters(values: IType | IType[]): IFunctionType; 27 | // returns(value: IType): IFunctionType; 28 | // } 29 | 30 | export type TypeCtor> = (schema?: Partial) => R; 31 | -------------------------------------------------------------------------------- /packages/compiler/utils/comment-parser.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as parser from 'comment-parser'; 3 | 4 | export function parseComment(content: string) { 5 | let matchs = (/\/\*((\s|.)*?)\*\//g).exec(content); 6 | if (matchs) { 7 | const doc = parser(matchs[0]); 8 | return doc.length ? doc[0].description : ''; 9 | } 10 | matchs = (/\/\/(.*)/g).exec(content); 11 | if (matchs) { 12 | return matchs[1].trim(); 13 | } 14 | } 15 | 16 | export function getComment(node: ts.Node, parse: boolean = true, multiline: boolean = true) { 17 | try { 18 | if (node.pos < 0 || node.end < 0) { 19 | return ''; 20 | } 21 | const text = node.getFullText(); 22 | const rang = ts.getLeadingCommentRanges(text, 0); 23 | if (rang && rang.length > 0) { 24 | let comment = text.substring(rang[0].pos, rang[0].end); 25 | if (!parse) { 26 | return comment.trim(); 27 | } 28 | comment = parseComment(comment); 29 | if (multiline) { 30 | return comment; 31 | } 32 | const arr = comment.split('\n'); 33 | return arr.length > 1 ? arr[0] : comment; 34 | } 35 | 36 | } catch (err) { 37 | 38 | } 39 | 40 | return ''; 41 | } 42 | -------------------------------------------------------------------------------- /packages/cli/gulpfile.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import gulp = require('gulp'); 3 | import ts = require('gulp-typescript'); 4 | import merge = require('merge2'); 5 | import { EasyTransformer } from '@easytype/compiler'; 6 | 7 | const argv = yargs 8 | .option('dist', { 9 | default: 'dist', 10 | type: 'string', 11 | }) 12 | .argv; 13 | 14 | const tsProject = ts.createProject('tsconfig.json', { 15 | emitDecoratorMetadata: true, 16 | experimentalDecorators: true, 17 | declaration: false, 18 | getCustomTransformers: () => ({ 19 | before: [ 20 | EasyTransformer(), 21 | ] 22 | }) 23 | }); 24 | const dtsProject = ts.createProject('tsconfig.json'); 25 | const outPut = tsProject.options.outDir || argv.dist; 26 | 27 | gulp.task('copy', function () { 28 | return gulp 29 | .src([`package.json`, `Readme.md`], { allowEmpty: true }) 30 | .pipe(gulp.dest(outPut)); 31 | }); 32 | 33 | gulp.task('build', function () { 34 | const tsResult = tsProject.src() 35 | .pipe(tsProject(ts.reporter.longReporter())) 36 | .js.pipe(gulp.dest(outPut)); 37 | 38 | if (dtsProject.options.declaration) { 39 | return merge([ 40 | dtsProject.src() 41 | .pipe(dtsProject()) 42 | .dts.pipe(gulp.dest(outPut)), 43 | tsResult 44 | ]); 45 | } else { 46 | return tsResult; 47 | } 48 | }); 49 | 50 | gulp.task('default', gulp.series('build', 'copy')); 51 | -------------------------------------------------------------------------------- /packages/compiler/translators/import.translator.ts: -------------------------------------------------------------------------------- 1 | import { Undefinable } from '@easytype/core'; 2 | import * as ts from 'typescript'; 3 | import { PACKAGE_NAME, PACKAGE_NAMESPACE } from '../constants'; 4 | import { ITranslator } from '../interfaces/translator.interface'; 5 | 6 | export const ImportTranslator: ITranslator = (node: ts.Node): Undefinable => { 7 | if (ts.isSourceFile(node)) { 8 | createImportDeclaration(node, PACKAGE_NAMESPACE, PACKAGE_NAME); 9 | return; 10 | } 11 | }; 12 | 13 | const imports: Map = new Map(); 14 | 15 | export function createImportDeclaration(file: ts.SourceFile, clause: string, module: string) { 16 | if (imports.get(file)) { 17 | return null; 18 | } 19 | imports.set(file, true); 20 | 21 | const exp = ts.createImportDeclaration( 22 | undefined, 23 | undefined, 24 | ts.createImportClause( 25 | undefined, 26 | ts.createNamespaceImport(ts.createIdentifier(clause)) 27 | ), 28 | ts.createStringLiteral(module) 29 | ); 30 | // for (const statement of file.statements) { 31 | // if (ts.isModuleDeclaration(statement)) { 32 | // statement.modifiers = ts.createNodeArray([ts.createModifier(ts.SyntaxKind.ExportKeyword)]); 33 | // } 34 | // } 35 | 36 | file.statements = ts.createNodeArray([exp, ...file.statements]); 37 | return exp; 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easytype", 3 | "version": "0.0.1", 4 | "description": "Runtime type system based on typescript.", 5 | "author": "davan.c", 6 | "license": "ISC", 7 | "main": "index.js", 8 | "scripts": { 9 | "build": "gulp core && gulp compiler", 10 | "watch": "gulp watch", 11 | "update": "npm-check -u -s && npm run link", 12 | "link": "npm link @easytype/core @easytype/compiler" 13 | }, 14 | "dependencies": { 15 | "@phenomnomnominal/tsquery": "^4.0.0", 16 | "chalk": "^3.0.0", 17 | "code-block-writer": "^10.0.0", 18 | "commander": "^4.0.1", 19 | "comment-parser": "^0.7.5", 20 | "fs-extra": "^8.1.0", 21 | "gulp-shell": "^0.7.1", 22 | "inversify": "^5.0.1", 23 | "lodash": "^4.17.15", 24 | "merge2": "^1.3.0", 25 | "moment": "^2.24.0", 26 | "reflect-metadata": "^0.1.13", 27 | "rxjs": "^6.5.3", 28 | "yargs": "^15.3.0" 29 | }, 30 | "devDependencies": { 31 | "@nestjs/testing": "^6.8.0", 32 | "@types/chai": "^4.2.3", 33 | "@types/fs-extra": "^8.0.0", 34 | "@types/lodash": "^4.14.142", 35 | "@types/mocha": "^5.2.7", 36 | "@types/node": "^12.7.8", 37 | "chai": "^4.2.0", 38 | "delete-empty": "^3.0.0", 39 | "gulp": "^4.0.2", 40 | "gulp-clean": "^0.4.0", 41 | "gulp-sourcemaps": "^2.6.5", 42 | "gulp-typescript": "^5.0.1", 43 | "mocha": "^6.2.0", 44 | "source-map-support": "^0.5.13", 45 | "ts-node": "^8.5.2", 46 | "tsconfig-paths": "^3.9.0", 47 | "typescript": "^3.8.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { spawn } from 'child_process'; 3 | import * as yargs from 'yargs'; 4 | import { ModuleInfo, resolveModules } from '../help'; 5 | 6 | const modules: ModuleInfo[] = [ 7 | { 8 | name: '@easytype/compiler', 9 | path: '@easytype/compiler/register/transpile-only', 10 | require: true 11 | }, 12 | { 13 | name: 'gulp', 14 | path: 'gulp/bin/gulp.js', 15 | require: true 16 | } 17 | ]; 18 | 19 | export default { 20 | command: 'build', 21 | desc: 'compile project', 22 | builder: (y) => { 23 | return y 24 | .option('output', { 25 | alias: 'o', 26 | describe: 'output directory', 27 | type: 'string' 28 | }) 29 | .default('output', 'dist'); 30 | }, 31 | handler: (argv) => { 32 | let bin: string; 33 | resolveModules(modules, (module) => bin = module.path); 34 | 35 | const file = join(__dirname, '../gulpfile.js'); 36 | const command = 'node'; 37 | const args = [ 38 | bin, 39 | '--gulpfile', 40 | file, 41 | '--cwd', 42 | process.cwd(), 43 | '--dist', 44 | argv.output as string 45 | ]; 46 | 47 | if (argv.debug) { 48 | console.log(args.join(' ')); 49 | } 50 | 51 | spawn(command, args, { 52 | stdio: 'inherit', 53 | shell: true 54 | }); 55 | } 56 | } as yargs.CommandModule; 57 | -------------------------------------------------------------------------------- /packages/core/types/enum.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { EnumSchema, IType } from '../interfaces'; 3 | import { isNil, isNumber, isString } from '../utils'; 4 | import { SchemaError } from '../errors'; 5 | 6 | export function EnumType(schema: Partial = {}): IType { 7 | schema.type = schema.type || DataType.STRING; 8 | schema.format = DataType.ENUM; 9 | schema.$type = DataType.ENUM; 10 | 11 | if (!Array.isArray(schema.enum) || !schema.enum.length) { 12 | throw new SchemaError(schema, `the value of prop 'enum' must be a non-empty array.`); 13 | } 14 | 15 | if (schema.type === DataType.STRING 16 | && schema.enum.some(v => !isString(v))) { 17 | throw new SchemaError(schema, `each value of prop 'keys' must be a string.`); 18 | } 19 | 20 | if (schema.type === DataType.NUMBER 21 | && schema.enum.some(v => !isNumber(v))) { 22 | throw new SchemaError(schema, `each value of prop 'keys' must be a number.`); 23 | } 24 | 25 | return { 26 | type: DataType.ENUM, 27 | schema: () => { 28 | return schema as EnumSchema; 29 | }, 30 | is: (value: any) => { 31 | return !schema.enum || (schema.enum as any[]).includes(value); 32 | }, 33 | value: (value: any) => { 34 | if (isNil(value)) { 35 | return; 36 | } 37 | if (schema.enum.some(p => p === value)) { 38 | return value; 39 | } 40 | return; 41 | } 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/types/array.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { ArraySchema, IArrayType, IType } from '../interfaces'; 3 | import { isNil } from '../utils'; 4 | 5 | export function ArrayType(schema: Partial = {}): IArrayType { 6 | schema.type = schema.type || DataType.ARRAY; 7 | 8 | return { 9 | type: DataType.ARRAY, 10 | schema: () => { 11 | if (!schema.items) { 12 | throw new Error(`'${schema.$id}' schema.items should not empty or undefined.`); 13 | } 14 | return schema as ArraySchema; 15 | }, 16 | is: (value: any) => { 17 | return value instanceof Array; 18 | }, 19 | value: (value: any) => { 20 | if (isNil(value)) { 21 | return; 22 | } 23 | if (Array.isArray(value)) { 24 | return value; 25 | } 26 | return [value]; 27 | }, 28 | items: (values: any) => { 29 | values = Array.isArray(values) ? values : [values] as IType[]; 30 | for (const value of values) { 31 | if (Array.isArray(schema.items)) { 32 | schema.items.push(value.schema()); 33 | } else { 34 | schema.items = value.schema(); 35 | } 36 | } 37 | 38 | return ArrayType(schema); 39 | }, 40 | contains: (values) => { 41 | values = Array.isArray(values) ? values : [values]; 42 | schema.contains = schema.contains || []; 43 | schema.contains.push(...values.map(value => value.schema())); 44 | return ArrayType(schema); 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/compiler/utils/ts-utils.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral } from '@easytype/core'; 2 | import * as ts from 'typescript'; 3 | import { PACKAGE_NAMESPACE, TS_METADATA, REFLECT_METADATA } from '../constants'; 4 | 5 | export function createObjectLiteral(obj: ObjectLiteral, multiLine?: boolean) { 6 | const props = []; 7 | for (const key in obj) { 8 | props.push(ts.createPropertyAssignment( 9 | ts.createIdentifier(key), 10 | obj[key] 11 | )); 12 | } 13 | return ts.createObjectLiteral(props, multiLine); 14 | } 15 | 16 | export function addMethodDecorator(node: ts.Node, key: string, exp: ts.Expression) { 17 | const decorator = ts.createDecorator(ts.createCall( 18 | ts.createIdentifier(TS_METADATA), 19 | undefined, 20 | [ 21 | ts.createStringLiteral(key), 22 | exp, 23 | ] 24 | )); 25 | (node.decorators as any as any[]).push(decorator); 26 | } 27 | 28 | export function isReflectableDecorator(decorator: ts.Decorator) { 29 | // @Reflectable() 30 | return ts.isCallExpression(decorator.expression) 31 | && ts.isIdentifier(decorator.expression.expression) 32 | && REFLECT_METADATA.includes(decorator.expression.expression.escapedText.toString()) 33 | && (!decorator.expression.arguments || !decorator.expression.arguments.length); 34 | } 35 | 36 | export function addDecorator(node: ts.Node, id: string, params: ts.Expression[] = []) { 37 | const decorator = ts.createDecorator(ts.createCall( 38 | ts.createPropertyAccess( 39 | ts.createIdentifier(PACKAGE_NAMESPACE), 40 | ts.createIdentifier(id) 41 | ), 42 | undefined, 43 | [...params] 44 | )); 45 | 46 | if (!node.decorators) { 47 | node.decorators = ts.createNodeArray([decorator]); 48 | return; 49 | } 50 | 51 | (node.decorators as any as any[]).push(decorator); 52 | } 53 | -------------------------------------------------------------------------------- /packages/core/types/date.type.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import { DataType } from '../enums'; 3 | import { DateSchema, IType } from '../interfaces'; 4 | import { SchemaError } from '../errors'; 5 | 6 | export function DateType(schema: Partial = {}): IType { 7 | schema.type = schema.type || DataType.NUMBER; 8 | schema.format = schema.format || DataType.DATE_TIME; 9 | schema.$type = DataType.DATE_TIME; 10 | if (schema.pattern && schema.type !== DataType.STRING) { 11 | throw new SchemaError(schema, `DateType Error: If the 'pattern' keyword is specified, the type must be 'STRING'.`); 12 | } 13 | schema.$value = schema.$value || DataType.DATE_TIME; 14 | 15 | return { 16 | type: DataType.DATE_TIME, 17 | schema: () => { 18 | return schema as DateSchema; 19 | }, 20 | is: (value: any) => { 21 | if (value instanceof Date && !isNaN(value.getTime())) { 22 | return true; 23 | } 24 | if (typeof value === 'number' && !isNaN(value) && moment(value).isValid()) { 25 | return true; 26 | } 27 | return false; 28 | }, 29 | value: (value: any) => { 30 | if (value) { 31 | try { 32 | const date = moment(value, schema.pattern); 33 | if (date.isValid()) { 34 | switch (schema.$value) { 35 | case DataType.NUMBER: 36 | return date.valueOf(); 37 | case DataType.STRING: 38 | return date.format(schema.pattern); 39 | case DataType.DATE_TIME: 40 | return date.toDate(); 41 | } 42 | } 43 | } catch (err) { 44 | return; 45 | } 46 | } 47 | return; 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/types/type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { TypeCtor } from '../interfaces'; 3 | import { AnyType } from './any.type'; 4 | import { ArrayType } from './array.type'; 5 | import { BigIntType } from './big-int.type'; 6 | import { BooleanType } from './boolean.type'; 7 | import { DateType } from './date.type'; 8 | import { EnumType } from './enum.type'; 9 | import { FloatType } from './float.type'; 10 | import { IntegerType } from './integer.type'; 11 | import { NumberType } from './number.type'; 12 | import { ObjectType } from './object.type'; 13 | import { RefType } from './ref.type'; 14 | import { SchemaType } from './schema.type'; 15 | import { StringType } from './string.type'; 16 | 17 | export class Type { 18 | public static Schema = SchemaType; 19 | 20 | public static Object = ObjectType; 21 | 22 | public static Ref = RefType; 23 | 24 | public static Any = AnyType; 25 | 26 | public static Array = ArrayType; 27 | 28 | public static String = StringType; 29 | 30 | public static Number = NumberType; 31 | 32 | public static Boolean = BooleanType; 33 | 34 | public static Integer = IntegerType; 35 | 36 | public static Float = FloatType; 37 | 38 | public static BigInt = BigIntType; 39 | 40 | public static Date = DateType; 41 | 42 | public static Enum = EnumType; 43 | 44 | private static readonly registers: { [key: string]: TypeCtor } = { 45 | [DataType.ANY]: AnyType, 46 | [DataType.OBJECT]: ObjectType, 47 | [DataType.ARRAY]: ArrayType, 48 | 49 | [DataType.STRING]: StringType, 50 | [DataType.NUMBER]: NumberType, 51 | [DataType.BOOLEAN]: BooleanType, 52 | [DataType.INTEGER]: IntegerType, 53 | [DataType.FLOAT]: FloatType, 54 | [DataType.DATE_TIME]: DateType, 55 | [DataType.ENUM]: EnumType, 56 | }; 57 | 58 | public static set(name: string, type: TypeCtor) { 59 | Type.registers[name] = type; 60 | } 61 | 62 | public static get(name: DataType | string) { 63 | return Type.registers[name]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/compiler/constants.ts: -------------------------------------------------------------------------------- 1 | import { $I, $T, $TypeOf, $U, $V, Any, BigInt, Float, Integer, Null } from '@easytype/core'; 2 | 3 | export const IGNORE_FLAG = '@es-ignore'; 4 | 5 | export const PACKAGE_NAMESPACE = '$easy'; 6 | 7 | export const PACKAGE_NAME = '@easytype/core'; 8 | 9 | export const METADATA_FIELD_NAME = '$metadata'; 10 | 11 | export const TS_METADATA = '__metadata'; 12 | 13 | export const REFLECT_METADATA = ['Reflectable', 'Reflective']; 14 | 15 | export const INHERITS_INTERFACE = 'Inherits'; 16 | 17 | export const EXTENDS_METADATA = 'Extends'; 18 | 19 | export const INHERIT_METADATA = 'Inherit'; 20 | 21 | export const IS_TYPE_METADATA = 'IsType'; 22 | 23 | export const IS_OBJECT_METADATA = 'IsObject'; 24 | 25 | export const STRING_ID = 'String'; 26 | 27 | export const NUMBER_ID = 'Number'; 28 | 29 | export const BOOLEAN_ID = 'Boolean'; 30 | 31 | export const DATE_ID = 'Date'; 32 | 33 | export const INTEGER_ID = `${PACKAGE_NAMESPACE}.${Integer.name}`; 34 | 35 | export const FLOAT_ID = `${PACKAGE_NAMESPACE}.${Float.name}`; 36 | 37 | export const BIGINT_ID = `${PACKAGE_NAMESPACE}.${BigInt.name}`; 38 | 39 | export const ANY_ID = `${PACKAGE_NAMESPACE}.${Any.name}`; 40 | 41 | export const NULL_ID = `${PACKAGE_NAMESPACE}.${Null.name}`; 42 | 43 | export const GENERIC_TYPE_ID = `${PACKAGE_NAMESPACE}.${$T.name}`; 44 | 45 | export const GENERIC_VARIABLE_ID = `${PACKAGE_NAMESPACE}.${$V.name}`; 46 | 47 | export const TYPEOF_TYPE_ID = `${PACKAGE_NAMESPACE}.${$TypeOf.name}`; 48 | 49 | export const UNION_TYPE_ID = `${PACKAGE_NAMESPACE}.${$U.name}`; 50 | 51 | export const INTERSECTION_TYPE_ID = `${PACKAGE_NAMESPACE}.${$I.name}`; 52 | 53 | export const STRING_NAME = 'String'; 54 | 55 | export const NUMBER_NAME = 'Number'; 56 | 57 | export const BOOLEAN_NAME = 'Boolean'; 58 | 59 | export const DATE_NAME = 'Date'; 60 | 61 | export const INTEGER_NAME = 'Integer'; 62 | 63 | export const BIGINT_NAME = 'BigInt'; 64 | 65 | export const FLOAT_NAME = 'Float'; 66 | 67 | export const TYPE_KEYWORDS = ['Promise', 'Observable', 'Partial', 'Required', 'Readonly']; 68 | 69 | export const ARRAY_KEYWORDS = ['Array', 'ReadonlyArray', 'ArrayLike']; 70 | -------------------------------------------------------------------------------- /packages/core/schema/schema-definition.ts: -------------------------------------------------------------------------------- 1 | import { Class, JSONSchema } from '../interfaces'; 2 | import { GlobalFactory, isString, uuid } from '../utils'; 3 | 4 | export class SchemaDefinition { 5 | // id => schema 6 | private static readonly definitions = GlobalFactory.createMap('definitions'); 7 | 8 | // id => class 9 | private static readonly refs = GlobalFactory.createMap('refs'); 10 | 11 | // class => id 12 | private static readonly objs = GlobalFactory.createMap('objs'); 13 | 14 | public static getClass(id: string) { 15 | return this.refs.get(id); 16 | } 17 | 18 | public static getClassByRef(id: string) { 19 | const ref = this.getRef(id); 20 | return ref ? this.getClass(ref.$id) : null; 21 | } 22 | 23 | public static getRefs() { 24 | return this.refs.values(); 25 | } 26 | 27 | public static getRef(param: string | Class) { 28 | let id; 29 | if (isString(param)) { 30 | id = param; 31 | if (id.startsWith('#')) { 32 | const names = id.split('/'); 33 | id = names.length ? names[names.length - 1] : id; 34 | } 35 | return this.definitions.get(id); 36 | } else { 37 | const target = param as Function; 38 | if (!target) { 39 | return null; 40 | } 41 | id = this.objs.get(target as any); 42 | if (!id) { 43 | return null; 44 | } 45 | return this.definitions.get(id as string); 46 | } 47 | } 48 | 49 | public static addRef(target: Class, schema: JSONSchema): string { 50 | let id = this.objs.get(target); 51 | if (id) { 52 | return id; 53 | } 54 | 55 | id = target.name; 56 | // exists / rename 57 | if (this.refs.has(id)) { 58 | id = `${id}_${uuid(6)}`; 59 | } 60 | this.objs.set(target, id); 61 | this.refs.set(id, target); 62 | this.definitions.set(id, schema); 63 | return id; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/cli/commands/start.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import * as fs from 'fs'; 3 | import { ModuleInfo, resolveModules } from '../help'; 4 | import * as path from 'path'; 5 | import * as yargs from 'yargs'; 6 | 7 | const sourceDirs = ['./', './src']; 8 | const modules: ModuleInfo[] = [ 9 | { 10 | name: '@easytype/compiler', 11 | path: '@easytype/compiler/register/transpile-only', 12 | require: true 13 | }, 14 | { 15 | name: 'reflect-metadata', 16 | path: 'reflect-metadata/Reflect.js', 17 | require: true 18 | }, 19 | { 20 | name: 'tsconfig-paths', 21 | path: 'tsconfig-paths/register', 22 | require: false 23 | } 24 | ]; 25 | 26 | function getMainFile(mainFile): string { 27 | const files = mainFile.includes('.') ? [mainFile] : [`${mainFile}.js`, `${mainFile}.ts`]; 28 | const results = []; 29 | for (const dir of sourceDirs) { 30 | for (const file of files) { 31 | const full = path.resolve(`${dir}/${file}`); 32 | if (fs.existsSync(full)) { 33 | return full; 34 | } 35 | results.push(full); 36 | } 37 | } 38 | console.log(`ERROR: Unable to find '${mainFile}' in the following folder: \r\n`, results); 39 | process.exit(0); 40 | } 41 | 42 | export default { 43 | command: 'start [file] [...args]', 44 | aliases: ['run'], 45 | desc: 'startup project', 46 | builder: (y) => { 47 | return y 48 | .default('debug', false) 49 | .default('file', 'main'); 50 | }, 51 | handler: (argv) => { 52 | const mainFile = getMainFile(argv.file); 53 | const command = 'node'; 54 | const args = [mainFile, ...process.argv.splice(3)]; 55 | 56 | resolveModules(modules, (module) => args.unshift('--require', module.path)); 57 | 58 | if (argv.debug) { 59 | console.log(args); 60 | } 61 | 62 | const child = spawn(command, args, { 63 | stdio: 'inherit', 64 | shell: true 65 | }); 66 | 67 | if (argv.debug) { 68 | child.on('exit', code => { 69 | console.log('exit', code); 70 | }); 71 | } 72 | } 73 | } as yargs.CommandModule; 74 | -------------------------------------------------------------------------------- /packages/core/interfaces/special-types.interface.ts: -------------------------------------------------------------------------------- 1 | import { TypeId } from '../enums'; 2 | import { ObjectLiteral } from '../primitive-types'; 3 | import { TypeReference, Class, Fields } from './type.interface'; 4 | import { ParameterInfo } from './param-info.interface'; 5 | 6 | export interface GenericArg { 7 | $name: string; 8 | $default?: any; 9 | } 10 | 11 | export type GenericTypeArgType = TypeReference | GenericType | GenericVariable; 12 | 13 | export type GenericTypeArg = GenericTypeArgType | GenericTypeArgType[]; 14 | 15 | export interface InheritInfo { 16 | $target: TypeReference | SpecialType; 17 | $fields: string[]; 18 | } 19 | 20 | export interface SpecialType { 21 | $type: number; 22 | [key: string]: any; 23 | } 24 | 25 | export interface PropType extends SpecialType { 26 | $type: TypeId.PROP_TYPE; 27 | $ref: TypeReference; 28 | $description?: string; 29 | $readonly?: boolean; 30 | $optional?: boolean; 31 | $default?: any; 32 | } 33 | 34 | export interface ObjectType extends SpecialType { 35 | $type: TypeId.OBJECT_TYPE; 36 | $target?: Function; 37 | $id?: string; 38 | $index?: [TypeReference, TypeReference]; 39 | $args: GenericArg[]; 40 | $properties: ObjectLiteral; 41 | $description?: string; 42 | $inherits?: InheritInfo[]; 43 | $extends?: TypeReference | SpecialType; 44 | } 45 | 46 | export interface GenericType extends SpecialType { 47 | $type: TypeId.GENERIC_TYPE; 48 | $target: TypeReference; 49 | $args: GenericTypeArg; 50 | $description?: string; 51 | } 52 | 53 | export interface GenericVariable extends SpecialType { 54 | $type: TypeId.GENERIC_VARIABLE_TYPE; 55 | $name: string; 56 | } 57 | 58 | export interface TypeOfType extends SpecialType { 59 | $type: TypeId.TYPE_OF_TYPE; 60 | $value: any; 61 | } 62 | 63 | export interface UnionType extends SpecialType { 64 | $type: TypeId.UNION_TYPE; 65 | $refs: TypeReference[]; 66 | } 67 | 68 | export interface IntersectionType extends SpecialType { 69 | $type: TypeId.INTERSECTION_TYPE; 70 | $refs: TypeReference[]; 71 | } 72 | 73 | export interface FunctionType extends SpecialType { 74 | $type: TypeId.FUNCTION; 75 | $decorators: Function[]; 76 | $returns: TypeReference; 77 | $params: ParameterInfo[]; 78 | $modifiers: number[]; 79 | $description?: string; 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/schema/schema-visitor.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeError } from '../errors'; 2 | import { ArraySchema, ObjectSchema, RefSchema, ISchemaVisitor, JSONSchema } from '../interfaces'; 3 | import { isFunction } from '../utils'; 4 | import { Schema } from './schema'; 5 | 6 | export abstract class SchemaVisitor { 7 | protected visitor: ISchemaVisitor; 8 | 9 | constructor() { 10 | this.visitor = this as any; 11 | } 12 | 13 | public visit(schema: Function | JSONSchema, context?: TContext, fieldKey?: string): TContext { 14 | if (isFunction(schema)) { 15 | const obj = Schema.getMetadata(schema as any) as JSONSchema; 16 | if (!obj) { 17 | throw new RuntimeError(`'${schema.name}' is not a valid schema.`); 18 | } 19 | schema = obj; 20 | } else { 21 | schema = schema as JSONSchema; 22 | } 23 | 24 | if (!Schema.isSchema(schema)) { 25 | throw new Error(`'${fieldKey}' is not a valid schema.`); 26 | } 27 | 28 | if (Schema.isObjectSchema(schema)) { 29 | return this.visitor.visitObject(fieldKey, schema, context); 30 | } else if (Schema.isRefSchema(schema)) { 31 | return this.visitor.visitRef(fieldKey, schema, context); 32 | } else if (Schema.isArraySchema(schema)) { 33 | return this.visitor.visitArray(fieldKey, schema, context); 34 | } else { 35 | return this.visitor.visitField(fieldKey, schema, context); 36 | } 37 | } 38 | 39 | protected getRefObject(ref: string): ObjectSchema { 40 | const schema = Schema.getRef(ref); 41 | if (!ref) { 42 | throw new Error(`Can not found schema ref: ${ref}`); 43 | } 44 | if (!Schema.isObjectSchema(schema)) { 45 | throw new Error(`Ref schema '${ref}' is not an Object Schema.`); 46 | } 47 | return schema; 48 | } 49 | 50 | protected visitEachChild(schema: ObjectSchema | ArraySchema | RefSchema, context?: TContext, fieldKey?: string): TContext { 51 | if (Schema.isRefSchema(schema)) { 52 | schema = this.getRefObject(schema.$ref); 53 | } 54 | 55 | if (Schema.isObjectSchema(schema)) { 56 | for (const key of Object.keys(schema.properties)) { 57 | const prop = schema.properties[key]; 58 | this.visit(prop, context, key); 59 | } 60 | return context; 61 | } 62 | if (Schema.isArraySchema(schema)) { 63 | this.visit(schema.items, context, fieldKey); 64 | return context; 65 | } 66 | 67 | return context; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/types/object.type.ts: -------------------------------------------------------------------------------- 1 | import { DataType } from '../enums'; 2 | import { SchemaError } from '../errors'; 3 | import { Fields, ObjectSchema, IObjectType, JSONSchema } from '../interfaces'; 4 | import { isObject, isString } from '../utils'; 5 | import _ = require('lodash'); 6 | 7 | export function ObjectType(schema: Partial = {}): IObjectType { 8 | schema.type = schema.type || DataType.OBJECT; 9 | schema.properties = schema.properties || {}; 10 | schema.required = schema.required || []; 11 | 12 | return { 13 | type: DataType.OBJECT, 14 | schema() { 15 | return schema as ObjectSchema; 16 | }, 17 | is(value: any) { 18 | return typeof value === 'object'; 19 | }, 20 | value(value: any) { 21 | if (isObject(value)) { 22 | return value; 23 | } 24 | if (isString(value)) { 25 | try { 26 | return JSON.parse(value); 27 | } catch (err) { 28 | return; 29 | } 30 | } 31 | return; 32 | }, 33 | setId(id: string) { 34 | schema.$id = id; 35 | }, 36 | description(text: string) { 37 | schema.description = text; 38 | }, 39 | prop(name: Fields | string, propSchema: Partial, required: boolean = true) { 40 | if (!isString(name)) { 41 | throw new SchemaError(schema, `object's prop name must be string.`); 42 | } 43 | 44 | // merge prop schema 45 | let prop = schema.properties[name]; 46 | if (prop) { 47 | prop = _.assign(prop, propSchema); 48 | } else { 49 | prop = propSchema as JSONSchema; 50 | } 51 | schema.properties[name] = prop; 52 | 53 | if (prop.optional) { 54 | return this.optional(name); 55 | } 56 | 57 | if (required) { 58 | this.required(name); 59 | } 60 | }, 61 | required(names: string | string[]) { 62 | names = Array.isArray(names) ? names : [names]; 63 | for (const field of names) { 64 | if (!schema.required.includes(field)) { 65 | schema.required.push(field); 66 | } 67 | } 68 | }, 69 | optional(names: string | string[]) { 70 | names = Array.isArray(names) ? names : [names]; 71 | for (const field of names) { 72 | const pos = schema.required.indexOf(field); 73 | if (pos === -1) { 74 | continue; 75 | } 76 | schema.required.splice(pos, 1); 77 | } 78 | }, 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /packages/core/utils/type-check.ts: -------------------------------------------------------------------------------- 1 | import { TYPE_SYMBOL } from '../constants'; 2 | import { DataType, TypeId } from '../enums'; 3 | import { Enum, GenericType, IType, ObjectType, PropType, SpecialType, TypeOfType, UnionType, IntersectionType, FunctionType } from '../interfaces'; 4 | 5 | export function isPromise(obj: any): obj is Promise { 6 | return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; 7 | } 8 | 9 | export function isUndefined(obj: any): obj is undefined { 10 | return typeof obj === 'undefined'; 11 | } 12 | 13 | export function isNil(obj: any): boolean { 14 | return typeof obj === 'undefined' || obj === null; 15 | } 16 | 17 | export function isEmpty(array: any): boolean { 18 | return !(array && array.length > 0); 19 | } 20 | 21 | export function isObject(value: any): boolean { 22 | return !isNil(value) && typeof value === 'object'; 23 | } 24 | 25 | export function isFunction(func: any): func is Function { 26 | return typeof func === 'function'; 27 | } 28 | 29 | export function isString(value: any): value is string { 30 | return typeof value === 'string'; 31 | } 32 | 33 | export function isNumber(value: any): value is number { 34 | return typeof value === 'number'; 35 | } 36 | 37 | export function isBigInt(value: any): value is BigInt { 38 | return typeof value === 'bigint'; 39 | } 40 | 41 | export function isBoolean(value: any): value is boolean { 42 | return typeof value === 'boolean'; 43 | } 44 | 45 | export function isInstance(value: any): boolean { 46 | return isObject(value) && isFunction(value.constructor); 47 | } 48 | 49 | export function isSymbol(value: any): value is symbol { 50 | return typeof value === 'symbol'; 51 | } 52 | 53 | export function isType(obj: any): obj is IType { 54 | return isObject(obj) && isFunction(obj.schema); 55 | } 56 | 57 | export function isEnum(value: any): value is Enum { 58 | return value 59 | && isFunction(value) 60 | && value[TYPE_SYMBOL] === DataType.ENUM; 61 | } 62 | 63 | export function isSpecialType(obj: any): obj is SpecialType { 64 | return isObject(obj) 65 | && obj.$type; 66 | } 67 | 68 | export function isObjectType(obj: any): obj is ObjectType { 69 | return isSpecialType(obj) 70 | && obj.$type === TypeId.OBJECT_TYPE 71 | && obj.$properties; 72 | } 73 | 74 | export function isPropType(obj: any): obj is PropType { 75 | return isSpecialType(obj) 76 | && obj.$type === TypeId.PROP_TYPE 77 | && obj.$ref; 78 | } 79 | 80 | export function isGenericType(obj: any): obj is GenericType { 81 | return isSpecialType(obj) 82 | && obj.$type === TypeId.GENERIC_TYPE 83 | && obj.$target 84 | && obj.$args; 85 | } 86 | 87 | export function isTypeOfType(obj: any): obj is TypeOfType { 88 | return isSpecialType(obj) 89 | && obj.$type === TypeId.TYPE_OF_TYPE 90 | && obj.$value; 91 | } 92 | 93 | export function isUnionType(obj: any): obj is UnionType { 94 | return isSpecialType(obj) 95 | && obj.$type === TypeId.UNION_TYPE 96 | && obj.$refs; 97 | } 98 | 99 | export function isIntersectionType(obj: any): obj is IntersectionType { 100 | return isSpecialType(obj) 101 | && obj.$type === TypeId.INTERSECTION_TYPE 102 | && obj.$refs; 103 | } 104 | 105 | export function isFunctionType(obj: any): obj is FunctionType { 106 | return isSpecialType(obj) 107 | && obj.$type === TypeId.FUNCTION 108 | && obj.$decorators 109 | && obj.$returns 110 | && obj.$params 111 | && obj.$modifiers; 112 | } 113 | -------------------------------------------------------------------------------- /packages/core/interfaces/schema/json-schema.interface.ts: -------------------------------------------------------------------------------- 1 | import { DataType, NumberFormats, StringFormats } from '../../enums'; 2 | import { ObjectLiteral } from '../../primitive-types'; 3 | 4 | export type SchemaObject = ObjectLiteral; 5 | 6 | /** 7 | * JSON Schema Annotations 8 | */ 9 | export interface BaseSchema { 10 | type?: DataType | string; 11 | $type?: string; 12 | 13 | /** descriptive */ 14 | $id?: string; 15 | title?: string; 16 | description?: string; 17 | examples?: TValue[]; 18 | 19 | /** condition */ 20 | not?: JSONSchema | JSONSchema[]; 21 | 22 | /** access attributes */ 23 | readOnly?: boolean; 24 | writeOnly?: boolean; 25 | 26 | /** value */ 27 | optional?: boolean; 28 | default?: TValue; 29 | 30 | $defs?: SchemaObject; 31 | } 32 | 33 | export interface ComplexSchema extends BaseSchema { 34 | type: never; 35 | } 36 | 37 | export interface RefSchema extends ComplexSchema { 38 | $ref: string; 39 | } 40 | 41 | export interface OneOfSchema extends ComplexSchema { 42 | oneOf?: JSONSchema[]; 43 | } 44 | 45 | export interface AllOfSchema extends ComplexSchema { 46 | allOf?: JSONSchema[]; 47 | } 48 | 49 | export interface AnyOfSchema extends ComplexSchema { 50 | anyOf?: JSONSchema[]; 51 | } 52 | 53 | export interface AnySchema extends ComplexSchema { 54 | $type: DataType.ANY; 55 | } 56 | 57 | export interface NullSchema extends ComplexSchema { 58 | $type: DataType.NULL; 59 | } 60 | 61 | export interface ObjectSchema extends BaseSchema { 62 | type: DataType.OBJECT; 63 | properties: SchemaObject; 64 | required?: string[]; 65 | additionalProperties?: boolean | JSONSchema; 66 | 67 | minProperties?: number; 68 | maxProperties?: number; 69 | 70 | $expose?: boolean; 71 | 72 | /** Base Class */ 73 | $x?: string; 74 | 75 | /** Generic Class */ 76 | $args?: string[]; 77 | } 78 | 79 | export interface ArraySchema extends BaseSchema { 80 | type: DataType.ARRAY; 81 | items: JSONSchema; 82 | minItems?: number; 83 | maxItems?: number; 84 | uniqueItems?: boolean; 85 | contains?: JSONSchema[]; 86 | } 87 | 88 | export interface StringSchema extends BaseSchema { 89 | type: DataType.STRING; 90 | format?: StringFormats | string; 91 | enum?: string[]; 92 | pattern?: string; 93 | maxLength?: number; 94 | minLength?: number; 95 | } 96 | 97 | export interface NumberSchema extends BaseSchema { 98 | type: DataType.NUMBER; 99 | format?: NumberFormats | string; 100 | enum?: number[]; 101 | multipleOf?: number; 102 | minimum?: number; 103 | maximum?: number; 104 | exclusiveMinimum?: number; 105 | exclusiveMaximum?: number; 106 | } 107 | 108 | export interface BooleanSchema extends BaseSchema { 109 | type: DataType.BOOLEAN; 110 | } 111 | 112 | export interface IntegerSchema extends NumberSchema { 113 | type: DataType.NUMBER; 114 | format: DataType.INTEGER; 115 | } 116 | 117 | export interface BigIntSchema extends NumberSchema { 118 | type: DataType.NUMBER; 119 | format: DataType.BIG_INT; 120 | } 121 | 122 | export interface FloatSchema extends NumberSchema { 123 | type: DataType.NUMBER; 124 | format: DataType.FLOAT; 125 | } 126 | 127 | export interface DoubleSchema extends NumberSchema { 128 | type: DataType.NUMBER; 129 | format: DataType.DOUBLE; 130 | } 131 | 132 | export interface DateSchema extends BaseSchema { 133 | type: DataType.NUMBER | DataType.STRING; 134 | format: DataType.DATE_TIME; 135 | $value: DataType.NUMBER | DataType.STRING | DataType.DATE_TIME; 136 | pattern?: string; 137 | } 138 | 139 | export interface EnumSchema extends BaseSchema { 140 | type: DataType.NUMBER | DataType.STRING; 141 | format: DataType.ENUM; 142 | enum: number[] | string[]; 143 | } 144 | 145 | export type JSONSchema = 146 | | AnySchema 147 | | NullSchema 148 | | ObjectSchema 149 | | ArraySchema 150 | | StringSchema 151 | | NumberSchema 152 | | BooleanSchema 153 | | IntegerSchema 154 | | BigIntSchema 155 | | FloatSchema 156 | | DoubleSchema 157 | | DateSchema 158 | | EnumSchema 159 | | RefSchema 160 | | OneOfSchema 161 | | AllOfSchema 162 | | AnyOfSchema 163 | ; 164 | -------------------------------------------------------------------------------- /packages/compiler/translators/enum.translator.ts: -------------------------------------------------------------------------------- 1 | import { DataType, EnumInfo, isNumber, SYMBOL_TYPE_KEY, Undefinable } from '@easytype/core'; 2 | import * as ts from 'typescript'; 3 | import { IGNORE_FLAG } from '../constants'; 4 | import { ITranslator } from '../interfaces/translator.interface'; 5 | import { getComment, TSNodeFactory } from '../utils'; 6 | 7 | export const EnumTranslator: ITranslator = (node: ts.Node): Undefinable => { 8 | if (!ts.isEnumDeclaration(node)) { 9 | return; 10 | } 11 | 12 | const info = getEnumInfo(node); 13 | if (!info) { 14 | return; 15 | } 16 | 17 | return getEnumClass(info); 18 | }; 19 | 20 | function getEnumInfo(node: ts.EnumDeclaration): EnumInfo { 21 | const comment = getComment(node, true, false); 22 | if (comment) { 23 | if (comment.includes(IGNORE_FLAG)) { 24 | return; 25 | } 26 | } 27 | 28 | const name = node.name.escapedText.toString(); 29 | const info: EnumInfo = { name, description: comment, fields: [] }; 30 | let index = 0; 31 | for (const member of node.members) { 32 | const key = (member.name as ts.Identifier).escapedText.toString(); 33 | const description = getComment(member, true, false); 34 | let value; 35 | if (member.initializer) { 36 | if (ts.isNumericLiteral(member.initializer)) { 37 | value = Number(member.initializer.text); 38 | } else if (ts.isPrefixUnaryExpression(member.initializer)) { // x = 1 + 1 39 | value = member.initializer.getText(); 40 | } else { 41 | value = member.initializer.getText(); 42 | } 43 | } else { 44 | const field = info.fields[index - 1]; 45 | value = (field && isNumber(field.value)) ? field.value + 1 : 0; 46 | } 47 | 48 | index++; 49 | info.fields.push({ key, value, description }); 50 | } 51 | 52 | return info; 53 | } 54 | 55 | function getEnumClass(info: EnumInfo) { 56 | const keys = info.fields.map(v => `'${v.key}'`); 57 | const values = info.fields.map(v => v.value); 58 | const fields = info.fields.map(v => `{ key: '${v.key}', value: ${v.value}, description: ${JSON.stringify(v.description)} }`); 59 | const props = info.fields.map(v => `static ${v.key} = ${v.value};`); 60 | 61 | const source = ` 62 | export class ${info.name} { 63 | ${props.join('\r\n')} 64 | static hasValue(value: number | string): boolean { 65 | return this.values.some(v => v === value); 66 | } 67 | 68 | static getKey(value: any): undefined | string { 69 | const field = this.fields 70 | .find(v => v.value === value); 71 | return field ? field.key : undefined; 72 | } 73 | 74 | static getKeys(value: any): string[] { 75 | return this.fields 76 | .filter(v => v.value === value) 77 | .map(v => v.key); 78 | } 79 | 80 | static hasKey(key: string): boolean { 81 | return this.keys.some(v => v === key); 82 | } 83 | 84 | static getDescription(key: string): undefined | string { 85 | const field = this.fields.find(v => v.key === key); 86 | return field ? field.description : undefined; 87 | } 88 | } 89 | Object.defineProperties(${info.name}, { 90 | [Symbol.for('${SYMBOL_TYPE_KEY}')]: { 91 | value: '${DataType.ENUM}', 92 | enumerable: true, 93 | configurable: false, 94 | writable: false 95 | }, 96 | 'description': { 97 | get: function () { 98 | return \`${info.description}\`; 99 | }, 100 | enumerable: true, 101 | configurable: false 102 | }, 103 | 'keys': { 104 | get: function () { 105 | return [${keys.join(', ')}]; 106 | }, 107 | enumerable: true, 108 | configurable: false 109 | }, 110 | 'values': { 111 | get: function () { 112 | return [${values.join(', ')}]; 113 | }, 114 | enumerable: true, 115 | configurable: false 116 | }, 117 | 'fields': { 118 | get: function () { 119 | return [${fields.join(', ')}]; 120 | }, 121 | enumerable: true, 122 | configurable: false 123 | } 124 | }); 125 | `; 126 | 127 | return TSNodeFactory.generateAst(source); 128 | } 129 | -------------------------------------------------------------------------------- /packages/core/decorators/schema.decorator.ts: -------------------------------------------------------------------------------- 1 | import { StringFormats } from '../enums'; 2 | import { JSONSchema, ObjectSchema, ObjectType } from '../interfaces'; 3 | import { Schema } from '../schema'; 4 | import { TypeFactory } from '../type-factory'; 5 | import { isObject } from '../utils'; 6 | import _ = require('lodash'); 7 | import { Type } from '../types'; 8 | 9 | function setProperty(metadata?: Partial): PropertyDecorator { 10 | return (target: any, propertyKey: string | symbol) => { 11 | Schema.setProperty(target.constructor, propertyKey as string, metadata); 12 | }; 13 | } 14 | 15 | export const IsSchema = (schema?: Partial, additional?: Partial): ClassDecorator => { 16 | return (target: any) => { 17 | if (schema && isObject(schema)) { 18 | Schema.setMetadata(target, schema as any); 19 | } 20 | 21 | if (additional) { 22 | const metadata = Schema.getMetadata(target); 23 | _.assign(metadata, additional); 24 | } 25 | }; 26 | }; 27 | 28 | export const Reflectable = (): ClassDecorator => IsSchema(); 29 | 30 | export const Reflective = (): ClassDecorator => IsSchema(); 31 | 32 | export const IsObject = (obj: ObjectType): PropertyDecorator => { 33 | return (target: any, propertyKey: string | symbol) => { 34 | const type = TypeFactory.getType(obj); 35 | Schema.setMetadata(target.constructor, type.schema() as ObjectSchema); 36 | }; 37 | }; 38 | 39 | export const Description = (description: string): any => { 40 | return (target: any, propertyKey?: string | symbol) => { 41 | if (propertyKey) { 42 | return setProperty({ description })(target, propertyKey); 43 | } 44 | const schema = Schema.getMetadata(target); 45 | schema.description = description; 46 | }; 47 | }; 48 | 49 | export const Format = (format: StringFormats | string): PropertyDecorator => setProperty({ format }); 50 | 51 | export const Title = (title: string): PropertyDecorator => setProperty({ title }); 52 | 53 | export const Examples = (examples: any[]): PropertyDecorator => setProperty({ examples }); 54 | 55 | export const ReadOnly = (): PropertyDecorator => setProperty({ readOnly: true }); 56 | 57 | export const WriteOnly = (): PropertyDecorator => setProperty({ writeOnly: true }); 58 | 59 | export const Optional = (): PropertyDecorator => { 60 | return (target: any, propertyKey: string | symbol) => { 61 | const schema = Schema.getMetadata(target.constructor); 62 | Type 63 | .Object(schema) 64 | .prop(propertyKey as string, { optional: true } as any); 65 | }; 66 | }; 67 | 68 | /** 69 | * '@Optional' decorator can be omitted. 70 | */ 71 | export const Default = (value: any): PropertyDecorator => { 72 | return (target: any, propertyKey: string | symbol) => { 73 | setProperty({ default: value })(target, propertyKey); 74 | Optional()(target, propertyKey); 75 | }; 76 | }; 77 | 78 | // export const Expose = (value: boolean = true) => IsSchema(null, { $expose: value }); 79 | 80 | // export const IsNull = (): PropertyDecorator => IsType(Null); 81 | 82 | // export const IsAny = (): PropertyDecorator => IsType(Any); 83 | 84 | // export const IsBoolean = (): PropertyDecorator => IsType(Boolean); 85 | 86 | // export const IsNumber = (): PropertyDecorator => IsType(Number); 87 | 88 | // export const IsInteger = (): PropertyDecorator => IsType(Integer); 89 | 90 | // export const IsBigInt = (): PropertyDecorator => IsType(BigInt); 91 | 92 | // export const IsFloat = (): PropertyDecorator => IsType(Float); 93 | 94 | // export const IsDouble = (): PropertyDecorator => IsType(Double); 95 | 96 | // export const IsString = (): PropertyDecorator => IsType(String); 97 | 98 | // export const IsDate = (valueType: Function = Date, format?: string): PropertyDecorator => { 99 | // const type = TypeFactory.createDateType(valueType, format); 100 | // return setProperty(type.schema()); 101 | // }; 102 | 103 | // export const IsTypeOf = (value: any): PropertyDecorator => { 104 | // if (isFunction(value)) { 105 | // return IsType(value); 106 | // } 107 | // if (isObject(value) && isFunction(value.constructor)) { 108 | // return IsType(value.constructor); 109 | // } 110 | // return IsAny(); 111 | // }; 112 | 113 | // export const IsArray = (obj: TypeReference): PropertyDecorator => IsType([obj]); 114 | 115 | 116 | // export const IsType = (ref?: TypeReference): PropertyDecorator => { 117 | // return (target: any, propertyKey: string | symbol) => { 118 | // if (!ref) { 119 | // ref = Reflect.getMetadata(TYPE_METADATA, target, propertyKey); 120 | // if (!isFunction(ref)) { 121 | // throw new RuntimeError(`@IsType Error: field '${propertyKey as string}' value type must be an instance of the class.`); 122 | // } 123 | // } 124 | 125 | // const root = Schema.getMetadata(target.constructor as any); 126 | // const type = TypeFactory.getType(ref, root); 127 | // setProperty(type.schema())(target, propertyKey); 128 | // }; 129 | // }; 130 | 131 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const gulp = require('gulp'); 4 | const ts = require('gulp-typescript'); 5 | const sourcemaps = require('gulp-sourcemaps'); 6 | const clean = require('gulp-clean'); 7 | const shell = require('gulp-shell') 8 | 9 | const source = 'packages'; 10 | const dist = './build'; 11 | const pkgPath = path.join(process.cwd(), source) 12 | const options = { verbose: true }; 13 | const rootPkgPath = path.join(process.cwd(), 'package.json'); 14 | 15 | const modules = fs 16 | .readdirSync(pkgPath) 17 | .filter( 18 | file => fs.statSync(path.join(pkgPath, file)).isDirectory() 19 | ); 20 | 21 | const packages = {}; 22 | const dependencies = {} 23 | 24 | function getPackageConfig (module) { 25 | const pkg_file = path.join(pkgPath, module, 'package.json'); 26 | if (fs.existsSync(pkg_file)) { 27 | return require(pkg_file); 28 | } 29 | return null; 30 | } 31 | 32 | function writePackageConfig (module, cfg) { 33 | const pkg_file = path.join(pkgPath, module, 'package.json'); 34 | fs.writeFileSync(pkg_file, JSON.stringify(cfg, null, 3)); 35 | } 36 | 37 | function addDependencies (module) { 38 | const cfg = getPackageConfig(module); 39 | if (cfg) { 40 | dependencies[cfg.name] = '^' + cfg.version; 41 | } 42 | const pkg = require(rootPkgPath); 43 | 44 | const deps = { ...pkg.dependencies, ...pkg.devDependencies }; 45 | for (const key in deps) { 46 | dependencies[key] = deps[key]; 47 | } 48 | } 49 | 50 | function addTSProjects (module) { 51 | let options = {}; 52 | packages[module] = ts.createProject(`packages/${module}/tsconfig.json`, options); 53 | } 54 | 55 | function addModuleTasks (module) { 56 | const _dependencies = {}; 57 | const sync = (deps) => { 58 | for (const key in deps) { 59 | const ver = dependencies[key]; 60 | if (ver) { 61 | deps[key] = ver; 62 | } 63 | _dependencies[key] = deps[key]; 64 | } 65 | } 66 | 67 | gulp.task(`${module}:sync`, (done) => { 68 | const pkg = require(rootPkgPath); 69 | const cfg = getPackageConfig(module); 70 | 71 | if (cfg) { 72 | sync(cfg.dependencies); 73 | sync(cfg.devDependencies); 74 | sync(cfg.peerDependencies); 75 | 76 | if (pkg) { 77 | cfg.description = pkg.description; 78 | cfg.author = pkg.author; 79 | cfg.license = pkg.license; 80 | } 81 | 82 | writePackageConfig(module, cfg); 83 | } 84 | console.log(_dependencies); 85 | done(); 86 | }); 87 | 88 | gulp.task(`${module}:build`, () => { 89 | return packages[module] 90 | .src() 91 | .pipe(packages[module]()) 92 | .pipe(gulp.dest(`${dist}/${module}`)); 93 | }); 94 | 95 | 96 | gulp.task(`${module}:copy`, () => { 97 | return gulp 98 | .src([`${source}/${module}/package.json`, `${source}/${module}/Readme.md`], { allowEmpty: true }) 99 | .pipe(gulp.dest(`${dist}/${module}`)) 100 | }); 101 | 102 | gulp.task(module, gulp.series( 103 | `${module}:sync`, 104 | `${module}:build`, 105 | shell.task(`cd ${source}/${module} && npm version patch `, options), 106 | `${module}:copy`, 107 | shell.task(`cd ${dist}/${module} && cnpm publish`, options) 108 | )); 109 | } 110 | 111 | modules.forEach(module => { 112 | addDependencies(module); 113 | addTSProjects(module); 114 | addModuleTasks(module) 115 | }) 116 | 117 | 118 | // 编译所有模块 119 | gulp.task('build', gulp.series(modules)); 120 | 121 | 122 | gulp.task('watch', function () { 123 | modules.forEach(module => { 124 | gulp.watch( 125 | [`${source}/${module}/**/*.ts`, `!${source}/${module}/**/*.d.ts`], 126 | gulp.series(module), 127 | ); 128 | }); 129 | }); 130 | 131 | 132 | gulp.task('clean:output', function () { 133 | return gulp 134 | .src( 135 | [`${source}/**/*.js`, `${source}/**/*.d.ts`, `${source}/**/*.js.map`, '!**/node_modules/**'], 136 | { 137 | read: false, 138 | }, 139 | ) 140 | .pipe(clean()); 141 | }); 142 | 143 | gulp.task('build:debug', async function () { 144 | const configurations = modules.map(module => { 145 | const register = ['api', 'common'].includes(module) 146 | ? "@easytype/compiler/register/transpile-only" 147 | : "ts-node/register/transpile-only"; 148 | 149 | return { 150 | "type": "node", 151 | "request": "launch", 152 | "name": `Run ${module[0].toUpperCase() + module.slice(1)} Tests`, 153 | "sourceMaps": true, 154 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 155 | "args": [ 156 | "--require", 157 | register, 158 | "--require", 159 | "'node_modules/reflect-metadata/Reflect.js'", 160 | "--timeout", 161 | "999999", 162 | "--colors", 163 | `\${workspaceFolder}/packages/${module}/**/*.spec.ts`, 164 | ], 165 | "console": "integratedTerminal" 166 | } 167 | }); 168 | 169 | const content = { 170 | "version": "0.2.0", 171 | "configurations": configurations 172 | } 173 | const file = path.join(process.cwd(), '.vscode', 'launch.json'); 174 | fs.writeFileSync(file, JSON.stringify(content, null, 3)); 175 | }); 176 | 177 | -------------------------------------------------------------------------------- /packages/core/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import 'reflect-metadata'; 3 | import { DataType } from '../enums'; 4 | import { Class, Fields, AllOfSchema, AnyOfSchema, ArraySchema, BigIntSchema, BooleanSchema, DateSchema, DoubleSchema, EnumSchema, FloatSchema, IntegerSchema, NumberSchema, ObjectSchema, OneOfSchema, RefSchema, BaseSchema, StringSchema, JSONSchema, Undefinable } from '../interfaces'; 5 | import { Type } from '../types'; 6 | import { isObject, isString } from '../utils'; 7 | import { SchemaDefinition } from './schema-definition'; 8 | 9 | export class Schema extends SchemaDefinition { 10 | 11 | public static setProperty(target: Class, propertyKey: Fields, metadata: Partial): void { 12 | const schema = Schema.getMetadata(target); 13 | if (!schema) { 14 | throw new Error(`metadata does not exist for field '${propertyKey}'`); 15 | // schema = Type.Object().schema(); 16 | // schema.$id = this.setMetadata(target, schema); 17 | } 18 | 19 | Type 20 | .Object(schema) 21 | .prop(propertyKey as string, metadata); 22 | } 23 | 24 | public static getProperty(target: Class, propertyKey: Fields | string): Undefinable { 25 | const schema = Schema.getMetadata(target); 26 | if (propertyKey && schema) { 27 | const prop = schema.properties[propertyKey as string]; 28 | return prop; 29 | } 30 | return schema; 31 | } 32 | 33 | public static getMetadata(target: Class, clone?: boolean): Undefinable { 34 | const schema: ObjectSchema = this.getRef(target) as ObjectSchema; 35 | if (!schema) { 36 | return schema; 37 | } 38 | 39 | return clone ? _.cloneDeep(schema) : schema; 40 | } 41 | 42 | public static setMetadata(target: Class, schema: ObjectSchema): string { 43 | return this.addRef(target, schema); 44 | } 45 | 46 | public static isSchema(schema: JSONSchema): schema is JSONSchema { 47 | return isObject(schema) 48 | && (isString(schema.type) 49 | || isString(schema.$type) 50 | || this.isRefSchema(schema) 51 | || this.isOneOfSchema(schema) 52 | || this.isAllOfSchema(schema) 53 | || this.isAnyOfSchema(schema) 54 | || isObject((schema as BaseSchema).not) 55 | ); 56 | } 57 | 58 | public static getDataType(schema: JSONSchema): string { 59 | if (Schema.isStringSchema(schema) 60 | && schema.format 61 | && Object.values(DataType).includes(schema.format as any) 62 | ) { 63 | return schema.format; 64 | } else { 65 | return schema.$type || schema.type || DataType.ANY; 66 | } 67 | } 68 | 69 | public static isRefSchema(schema: JSONSchema): schema is RefSchema { 70 | return schema && isString((schema as RefSchema).$ref); 71 | } 72 | 73 | public static isOneOfSchema(schema: JSONSchema): schema is OneOfSchema { 74 | return schema && isObject((schema as OneOfSchema).oneOf); 75 | } 76 | 77 | public static isAllOfSchema(schema: JSONSchema): schema is AllOfSchema { 78 | return schema && isObject((schema as AllOfSchema).allOf); 79 | } 80 | 81 | public static isAnyOfSchema(schema: JSONSchema): schema is AllOfSchema { 82 | return schema && isObject((schema as AnyOfSchema).anyOf); 83 | } 84 | 85 | public static isNumberSchema(schema: JSONSchema): schema is NumberSchema { 86 | return schema && schema.type === DataType.NUMBER; 87 | } 88 | 89 | public static isStringSchema(schema: JSONSchema): schema is StringSchema { 90 | return schema && schema.type === DataType.STRING; 91 | } 92 | 93 | public static isObjectSchema(schema: JSONSchema): schema is ObjectSchema { 94 | return schema && schema.type === DataType.OBJECT; 95 | } 96 | 97 | public static isArraySchema(schema: JSONSchema): schema is ArraySchema { 98 | return schema && schema.type === DataType.ARRAY; 99 | } 100 | 101 | public static isBooleanSchema(schema: JSONSchema): schema is BooleanSchema { 102 | return schema && schema.type === DataType.BOOLEAN; 103 | } 104 | 105 | public static isIntegerSchema(schema: JSONSchema): schema is IntegerSchema { 106 | return this.isNumberSchema(schema) && schema.format === DataType.INTEGER; 107 | } 108 | 109 | public static isBigIntSchema(schema: JSONSchema): schema is BigIntSchema { 110 | return this.isNumberSchema(schema) && schema.format === DataType.BIG_INT; 111 | } 112 | 113 | public static isFloatSchema(schema: JSONSchema): schema is FloatSchema { 114 | return this.isNumberSchema(schema) && schema.format === DataType.FLOAT; 115 | } 116 | 117 | public static isDoubleSchema(schema: JSONSchema): schema is DoubleSchema { 118 | return this.isNumberSchema(schema) && schema.format === DataType.DOUBLE; 119 | } 120 | 121 | public static isDateSchema(schema: JSONSchema): schema is DateSchema { 122 | return schema 123 | && (schema.type === DataType.STRING 124 | || schema.type === DataType.NUMBER) 125 | && ((schema as StringSchema).format === DataType.DATE_TIME); 126 | } 127 | 128 | public static isEnumSchema(schema: JSONSchema): schema is EnumSchema { 129 | return schema 130 | && (schema.type === DataType.STRING 131 | || schema.type === DataType.NUMBER) 132 | && ((schema as StringSchema).format === DataType.ENUM); 133 | } 134 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # 欢迎使用EasyType:一个基于TypeScript的动态类型反射系统 2 | 3 | 众所周知JavaScript因为语言的特性,无法与JAVA一样提供一种动态类型反射机制,而市面上又缺乏完善的解决方案,EasyType的出现是为了从根本上解决这个问题, 赋予开发者尤其是后端开发者更多的能力。 4 | 5 | ###### 警告:单元测试未完全覆盖,切勿用于商业项目。 6 | ###### 开源只是为了交流技术、不想把一个好的理念埋没在个人手里,由于个人时间关系,本项目可能不会得到良好的维护,期望有成熟的公司或者团队能够改进或者重构它,让他成为node后端必备框架之一。 7 | 8 | ### 项目起源 9 | 从18年开始,我就决定让团队使用node+typescript来开发后端服务,经过一年多的实践,发现各种库都有一套自己的“建模语言”来申明类型,比如mongoose、数据验证器、GraphQL、GRPC、swagger等等。这不禁让我迷惑不解,为什么用了typescript以后还要重复写这么多的类型申明?于是我从19年初开始开发了这个框架陆续来实现。刚开始是通过AST来分析mongoose的schema生成模型接口和定义,但是没有从根本上解决问题,所以接下来是通过直接分析typescript的类申明来实现,后续又引入了json-schema标准,最后对枚举、方法、联合类型、泛型都提供了支持。 10 | 11 | ### 设计目标 12 | - 能够覆盖typescript绝大多数的类型,尤其是对泛型能提供完善的支持。 13 | - 尽可能的减少侵入,无需改动任何的代码 14 | - 支持Transpile模式,无需构建即可直接运行,而且编译速度非常快 15 | - 能够通过cli运行与构建项目,也能够脱离cli运行或者构建项目 16 | 17 | ### 方案对比 18 | | 项目 | 对比 | 19 | | ------------ | ------------ | 20 | | [io-ts ](https://github.com/gcanti/io-ts "io-ts ")| 定义了一套类型声明,设计目标可能主要是解决IO传输中的编码与解码 | 21 | | [class-transformer](https://github.com/typestack/class-transformer "class-transformer") | 定义了一套修饰器,只支持部分的TS类型 | 22 | | [typescript-json-schema](https://github.com/YousefED/typescript-json-schema "typescript-json-schema") | 需要调用CLI生成JSON格式的类型声明,非动态 | 23 | | [tsruntime](https://github.com/goloveychuk/tsruntime "tsruntime") | 是和typescript-json-schema一样在TypeCheck阶段实现,因此不支持Transpile模式 | 24 | | [type-reflect](https://github.com/andywer/type-reflect "type-reflect") | 类似,但功能不够完善 | 25 | 26 | ### 使用场景 27 | 引入EasyType将为你的后端开发带来更大的想象空间,其中我们团队用到的部分就包括: 28 | - 不用添加任何代码,将TS类申明直接转换成mongoose schema 29 | - 不用添加任何代码,可以直接使用各种json-schema数据验证器 30 | - 不用添加任何代码,动态生成API文档,稍微改动就能生成OpenAPI规范的swagger文档,你的接口改动团队其他成员可以随时看到。 31 | - 不用添加任何代码,动态生成RPC声明,比如一键生成GRPC proto申明文件,把微服务开发变成一件很轻松的事情。 32 | - 由于后端能够输出完整的类型申明,因此前端(尤其是后台开发)能够快速的构建出数据显示、操作界面,会使开发变得更快速高效。 33 | 34 | ### 原理 35 | 通过typescript的自定义transform,在编译阶段把类型写入类描述中,最后在运行时生成json-schema标准的类型说明。 36 | ```javascript 37 | @Reflectable() 38 | export class User extends Document { 39 | /** 用户ID */ 40 | uid: number; 41 | 42 | /** 用户名 */ 43 | username: string; 44 | } 45 | ``` 46 | 比如以上代码,通过编译器将变为: 47 | ```javascript 48 | export class User extends Document { 49 | $easy.IsObject({ 50 | $type: 1, 51 | $properties: { 52 | uid: { 53 | $type: 2, 54 | $description: "\u7528\u6237ID", 55 | $ref: Number 56 | }, 57 | username: { 58 | $type: 2, 59 | $description: "\u7528\u6237\u540D", 60 | $ref: String 61 | }, 62 | }, 63 | $target: User, 64 | $id: "User", 65 | $extends: mongoose_1.Document 66 | }) 67 | private $metadata: any; 68 | } 69 | ``` 70 | 通过在运行时调用 Schema.getMetadata(User), 即可得到User的类型声明(json-schema): 71 | ```javascript 72 | { 73 | "type": "object", 74 | "properties": { 75 | "_id": { 76 | "type": "string", 77 | "format": "OBJECT_ID", 78 | "description": "ID" 79 | }, 80 | "uid": { 81 | "type": "number", 82 | "description": "用户ID", 83 | }, 84 | "username": { 85 | "type": "string", 86 | "description": "用户名", 87 | } 88 | }, 89 | "required": [ 90 | "_id", 91 | "uid", 92 | "username" 93 | ], 94 | "$id": "User", 95 | "$x": "Document" 96 | } 97 | ``` 98 | ### 更强大的ENUM 99 | 在typescript中enum的存在更像是为了描述类型(你在运行时很难分清哪个是key,哪个是value),这也就不能与JAVA一样提供一些操作, 因此EasyType在编译阶段增加一些方法,使之变得更加灵活和强大,借助这些特性我们就能够实现无缝输出ENUM信息到API文档,而无需再书写任何的注释或者代码。 100 | 101 | ```javascript 102 | export interface EnumInterface { 103 | readonly keys: string[]; 104 | 105 | readonly values: number[] | string[]; 106 | 107 | getValue(key: string): Undefinable; 108 | 109 | hasValue(value: any): boolean; 110 | 111 | getKeys(value: any): string[]; 112 | 113 | getKey(value: any): Undefinable; 114 | 115 | hasKey(key: string): boolean; 116 | 117 | getDescription(key: string): Undefinable; 118 | } 119 | 120 | export type Enum = 121 | { readonly [P in keyof T]: T[P]; } 122 | & Readonly 123 | & EnumInterface 124 | ; 125 | 126 | ``` 127 | 现在你可以通过 Enum<Foo>.Keys 和 Enum<Bar>.values 获得键值,也可以拿到对应的像这样的类型申明: 128 | ```javascript 129 | { 130 | "name": "AssetType", 131 | "description": "用户资产类型", 132 | "fields": [ 133 | { 134 | "key": "BALANCE", 135 | "value": 1, 136 | "description": "账户余额" 137 | }, 138 | { 139 | "key": "POINTS", 140 | "value": 3, 141 | "description": "账户积分" 142 | } 143 | ] 144 | }, 145 | ``` 146 | 147 | ### 部分继承 148 | 使用关键词extends会继承基类所有的属性,有时候如果你想部分继承基类属性,可以使用Inherits语法糖: 149 | ```javascript 150 | @Reflectable() 151 | export class UserLoginDto implements Inherits { 152 | username: string; 153 | password: string; 154 | } 155 | 156 | ``` 157 | ### 方法反射 158 | 方法的注释、修饰符、参数、返回值等信息会被标注到Metadata中,可以通过 Reflect.getMetadata('easy:metadata', target, propertyKey) 获取。 159 | 160 | ### 还有哪些问题? 161 | - 还没来得及做完整的单元测试,所以暂时不能用于商业项目 162 | - 泛型目前编译器这块已完成,但是运行时由于时间关系还未能提供支持 163 | - 由于设计原因,只能支持类的输出,不支持interface和type的输出,因为前者在js运行时以function存在,后者不存在于运行时,或许后面会想办法支持。 164 | - 低版本的TypeScript(3.7以下)会有一些问题,所以用最新的TSC编译吧。 165 | 166 | ### 插播一条广告 167 | 即将推出基于EasyType+nest.js的全家桶开发包,尽情期待。 168 | 项目地址: [https://github.com/davanchen/easynest](https://github.com/davanchen/easynest) 169 | 170 | ### 演示:借助vscode插件(即将开源)一键生成proto 171 | ![](https://tmyx.oss-cn-hangzhou.aliyuncs.com/github/Code_7GLTwoRGAx.png) 172 | ![](https://tmyx.oss-cn-hangzhou.aliyuncs.com/github/Code_oBYASf68Uo.png) 173 | 174 | ### 演示:EasyNest API文档模块自动生成API描述(枚举、控制器、模型) 175 | ![](https://tmyx.oss-cn-hangzhou.aliyuncs.com/github/screenshot_controller.PNG) 176 | ![](https://tmyx.oss-cn-hangzhou.aliyuncs.com/github/screenshot_enums.PNG) 177 | ![](https://tmyx.oss-cn-hangzhou.aliyuncs.com/github/screenshot_models.PNG) -------------------------------------------------------------------------------- /packages/core/type-factory.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { DataType } from './enums'; 3 | import { Class, Enum, GenericType, IObjectType, IType, JSONSchema, SpecialType, TypeReference, Undefinable, ObjectSchema, ObjectType } from './interfaces'; 4 | import { Null, ObjectLiteral } from './primitive-types'; 5 | import { Schema } from './schema'; 6 | import { Type } from './types'; 7 | import { isEnum, isFunction, isObjectType, isPropType, isSpecialType, isGenericType, isNil, isUndefined, isBoolean } from './utils'; 8 | 9 | export class TypeFactory { 10 | public static getDataType(type: TypeReference) { 11 | if (isFunction(type)) { 12 | if (type === String) { 13 | return DataType.STRING; 14 | } else if (type === Number) { 15 | return DataType.NUMBER; 16 | } else if (type === Boolean) { 17 | return DataType.BOOLEAN; 18 | } else if (type === Date) { 19 | return DataType.DATE_TIME; 20 | } else { 21 | if (isEnum(type)) { 22 | return DataType.ENUM; 23 | } 24 | return DataType.OBJECT; 25 | } 26 | } else if (Array.isArray(type)) { 27 | return DataType.ARRAY; 28 | } else { 29 | return; 30 | } 31 | } 32 | 33 | public static createDateType(valueType: Function = Date, format?: string) { 34 | return Type.Date({ 35 | $value: this.getDataType(valueType) as any, 36 | pattern: format 37 | }); 38 | } 39 | 40 | public static getEnumType(obj: Enum, valueType: TypeReference = String) { 41 | return Type.Enum({ 42 | $id: obj.name, 43 | type: this.getDataType(valueType) as any, 44 | enum: valueType === String ? obj.keys : obj.values 45 | }); 46 | } 47 | 48 | public static getGenericType(type: GenericType): Undefinable { 49 | // console.log(type); 50 | if (type.$target === Map) { 51 | 52 | } 53 | 54 | if (type.$target === ObjectLiteral) { 55 | 56 | } 57 | 58 | if (type.$target === Null) { 59 | 60 | } 61 | 62 | return Type.Object({ $id: type.$description }); // Type.Any(); 63 | } 64 | 65 | public static getObjectType(ref: ObjectType): Undefinable { 66 | const result = Schema.getRef(ref.$target as Class); 67 | if (result) { 68 | return Type.Object(result as ObjectSchema); 69 | } 70 | 71 | // extends 72 | let obj: IObjectType; 73 | if (ref.$extends) { 74 | const base = this.getReferenceType(ref.$extends); 75 | if (base) { 76 | const schema = _.cloneDeep(base.schema()) as ObjectSchema; 77 | if (schema.type !== DataType.OBJECT) { 78 | throw new Error(`'${ref.$id}' must extends from class`); 79 | } 80 | if (!schema.$id) { 81 | throw new Error(`'${ref.$id}' inherited class has no ID`); 82 | } 83 | schema.$x = schema.$id; 84 | obj = Type.Object(schema); 85 | } 86 | } else { 87 | obj = Type.Object({}); 88 | } 89 | 90 | if (ref.$id) { 91 | obj.setId(ref.$id); 92 | } 93 | 94 | // inherits 95 | if (ref.$inherits) { 96 | for (const inherits of ref.$inherits) { 97 | const base = this.getReferenceType(inherits.$target); 98 | if (base) { 99 | const schema = base.schema() as ObjectSchema; 100 | if (schema.type !== DataType.OBJECT) { 101 | throw new Error(`'${ref.$id}' must inherits from class`); 102 | } 103 | const fields = (inherits.$fields && inherits.$fields.length > 0) ? inherits.$fields : Object.keys(ref.$properties); 104 | for (const field of fields) { 105 | const prop = schema.properties[field]; 106 | if (prop) { 107 | obj.prop(field, _.cloneDeep(prop)); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | for (const key in ref.$properties) { 115 | const value = ref.$properties[key]; 116 | if (!isPropType(value)) { 117 | throw new Error(`prop '${key}' must be 'PropType'. `); 118 | } 119 | 120 | const type = this.getType(value.$ref, obj.schema()); 121 | if (!type) { 122 | throw new Error(`invalid type reference for prop '${key}'. `); 123 | } 124 | 125 | const schema: Partial = type.schema(); 126 | if (isBoolean(value.$optional)) { 127 | schema.optional = value.$optional; 128 | } 129 | if (isBoolean(value.$readonly)) { 130 | schema.readOnly = value.$readonly; 131 | } 132 | if (!isUndefined(value.$default)) { 133 | schema.default = value.$default; 134 | schema.optional = true; 135 | } 136 | if (value.$description) { 137 | schema.description = value.$description; 138 | } 139 | obj.prop(key, schema); 140 | } 141 | 142 | return obj; 143 | } 144 | 145 | public static getSpecialType(obj: SpecialType, root?: JSONSchema): Undefinable { 146 | if (isObjectType(obj)) { 147 | return this.getObjectType(obj); 148 | } 149 | 150 | if (isGenericType(obj)) { 151 | return this.getGenericType(obj) as any; 152 | } 153 | } 154 | 155 | public static getReferenceType(obj: TypeReference | SpecialType, root?: JSONSchema): Undefinable { 156 | if (isSpecialType(obj)) { 157 | return this.getSpecialType(obj); 158 | } 159 | const target = obj as Class; 160 | const ref = Schema.getMetadata(target); 161 | if (!ref) { 162 | throw new Error(`schema metadata for class '${target.name}' is missing.`); 163 | } 164 | return Type.Object(ref); 165 | } 166 | 167 | public static getType(obj: TypeReference | SpecialType, root?: JSONSchema): Undefinable { 168 | if (isSpecialType(obj)) { 169 | return this.getSpecialType(obj); 170 | } 171 | 172 | const type = this.getDataType(obj); 173 | if (!type) { 174 | const name = isFunction(obj) ? obj.name : obj; 175 | throw new Error(`object '${name}' is an unknown type.`); 176 | } 177 | 178 | switch (type) { 179 | case DataType.OBJECT: { 180 | const target = obj as Class; 181 | const ref = Schema.getMetadata(target); 182 | if (!ref) { 183 | throw new Error(`schema metadata for class '${target.name}' is missing.`); 184 | } 185 | // support third-party schema like { type: 'OBJECT_ID' } 186 | if (ref.type !== DataType.OBJECT || !ref.$id) { 187 | return Type.Schema(_.cloneDeep(ref)); 188 | } 189 | return Type.Ref(ref.$id, root, ref); 190 | } 191 | case DataType.ENUM: { 192 | const enumType = TypeFactory.getEnumType(obj as any, Number); 193 | if (enumType) { 194 | Schema.addRef(obj as any, enumType.schema()); 195 | } 196 | return enumType; 197 | } 198 | case DataType.ARRAY: { 199 | return Type 200 | .Array() 201 | .items(this.getType(obj[0], root)); 202 | } 203 | default: 204 | return Type.get(type)(); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /packages/compiler/translators/class.translator.ts: -------------------------------------------------------------------------------- 1 | import { AnyOf, EASY_METADATA, FunctionType, GenericArg, isEmpty, Nullable, ParameterInfo, TsModifier, TypeId, Undefinable, InheritInfo } from '@easytype/core'; 2 | import * as ts from 'typescript'; 3 | import { EXTENDS_METADATA, INHERITS_INTERFACE, INHERIT_METADATA, IS_OBJECT_METADATA, METADATA_FIELD_NAME } from '../constants'; 4 | import { ITranslator, ReflectContext } from '../interfaces'; 5 | import { addDecorator, addMethodDecorator, createObjectLiteral, getTypeExpression, getTypeLiteralTypeExpression, isReflectableDecorator, getComment, getObjectType } from '../utils'; 6 | 7 | export const ClassTranslator: ITranslator = (node: ts.Node): Undefinable => { 8 | if (ts.isImportDeclaration(node)) { 9 | return ts.getMutableClone(node); 10 | } 11 | 12 | if (!ts.isClassDeclaration(node)) { 13 | return; 14 | } 15 | 16 | if (isEmpty(node.decorators)) { 17 | return; 18 | } 19 | 20 | const context: ReflectContext = { 21 | reflectable: isClassReflectable(node), 22 | props: getPropertyNames(node), 23 | args: getClassTypeArgs(node) 24 | }; 25 | 26 | if (context.reflectable) { 27 | const metaNode: ts.Node = createMetadataPropertyNode(node); 28 | resolvePropertys(node, metaNode, context); 29 | } 30 | 31 | resolveMethods(node, context); 32 | return node; 33 | }; 34 | 35 | function createMetadataPropertyNode(node: ts.ClassDeclaration) { 36 | const prop = ts.createProperty( 37 | undefined, 38 | [ts.createModifier(ts.SyntaxKind.PrivateKeyword)], 39 | ts.createIdentifier(METADATA_FIELD_NAME), 40 | ts.createToken(ts.SyntaxKind.QuestionToken), 41 | ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), 42 | undefined 43 | ); 44 | prop.parent = node; 45 | prop.flags = 0; 46 | 47 | (node.members as any as any[]).unshift(prop); 48 | return prop; 49 | } 50 | 51 | // @Reflectable() 52 | function isClassReflectable(node: ts.ClassDeclaration) { 53 | if (!node.decorators.some((decorator) => isReflectableDecorator(decorator)) 54 | ) { 55 | return false; 56 | } 57 | return true; 58 | } 59 | 60 | function getClassTypeArgs(node: ts.ClassDeclaration): Nullable { 61 | if (!isEmpty(node.typeParameters)) { 62 | return node.typeParameters.map(type => { 63 | if (ts.isTypeParameterDeclaration(type) && ts.isIdentifier(type.name)) { 64 | const arg: GenericArg = { 65 | $name: type.name.text, 66 | }; 67 | 68 | if (type.default) { 69 | arg.$default = getTypeExpression(type.default); 70 | } 71 | 72 | return arg; 73 | } 74 | return null; 75 | }); 76 | } 77 | return null; 78 | } 79 | 80 | export function getPropertyNames(node: ts.ClassDeclaration) { 81 | const props: string[] = []; 82 | for (const member of node.members) { 83 | if (ts.isPropertyDeclaration(member)) { 84 | if (ts.isIdentifier(member.name)) { 85 | props.push(member.name.escapedText.toString()); 86 | } 87 | } 88 | } 89 | return props; 90 | } 91 | 92 | export function resolveMethods(node: ts.ClassDeclaration, context: ReflectContext) { 93 | node.members 94 | .filter(member => ts.isMethodDeclaration(member) && !isEmpty(member.decorators)) 95 | .forEach(member => resolveMethod(member as ts.MethodDeclaration, context)); 96 | } 97 | 98 | export function resolvePropertys(node: ts.ClassDeclaration, metaNode: Nullable, context: ReflectContext) { 99 | // class ? implements Inherits 100 | const { $extends, $inherits } = resolveHeritageClauses(node, context); 101 | 102 | const obj = getObjectType(node, context.args); 103 | if ($extends) { 104 | obj.$extends = $extends; 105 | } 106 | if ($inherits && $inherits.length) { 107 | obj.$inherits = ts.createArrayLiteral($inherits, true); 108 | } 109 | 110 | addDecorator(metaNode, IS_OBJECT_METADATA, [createObjectLiteral(obj, true)]); 111 | } 112 | 113 | function resolveInherits(node: ts.ClassDeclaration, clauseType: ts.ExpressionWithTypeArguments, context: ReflectContext): ts.Expression { 114 | const typeArgs = clauseType.typeArguments; 115 | if (isEmpty(typeArgs)) { 116 | return; 117 | } 118 | 119 | let fields: string[] = []; 120 | const [baseType, propsType] = typeArgs; 121 | 122 | if (typeArgs.length === 2) { 123 | // Inherits 124 | if (ts.isLiteralTypeNode(propsType) && ts.isStringLiteral(propsType.literal)) { 125 | fields = [propsType.literal.text]; 126 | } 127 | // Inherits 128 | if (ts.isUnionTypeNode(propsType)) { 129 | fields = propsType.types.map(type => 130 | ts.isLiteralTypeNode(type) 131 | && ts.isStringLiteral(type.literal) ? type.literal.text : null 132 | ); 133 | } 134 | } 135 | 136 | if (ts.isTypeReferenceNode(baseType) && ts.isIdentifier(baseType.typeName)) { 137 | const info: AnyOf = { 138 | $target: getTypeExpression(baseType, context.args), 139 | $fields: ts.createArrayLiteral( 140 | fields.map(prop => ts.createStringLiteral(prop)), 141 | false 142 | ) 143 | }; 144 | return createObjectLiteral(info, true); 145 | } 146 | } 147 | 148 | function resolveExtends(node: ts.ClassDeclaration, clauseType: ts.ExpressionWithTypeArguments, context: ReflectContext): ts.Expression { 149 | let type: ts.Node; 150 | if (!isEmpty(clauseType.typeArguments)) { 151 | type = ts.createTypeReferenceNode( 152 | ts.isIdentifier(clauseType.expression) 153 | ? clauseType.expression 154 | : ts.createIdentifier(''), 155 | clauseType.typeArguments 156 | ); 157 | } else { 158 | type = clauseType.expression; 159 | } 160 | 161 | return getTypeExpression(type, context.args); 162 | } 163 | 164 | function resolveHeritageClauses(node: ts.ClassDeclaration, context: ReflectContext) { 165 | if (!node.heritageClauses || !node.heritageClauses.length) { 166 | return {}; 167 | } 168 | let $extends: ts.Expression; 169 | const $inherits: ts.Expression[] = []; 170 | 171 | for (const clause of node.heritageClauses) { 172 | for (const clauseType of clause.types) { 173 | if (ts.isExpressionWithTypeArguments(clauseType) 174 | && ts.isIdentifier(clauseType.expression) 175 | ) { 176 | // extends 177 | if (clause.token === ts.SyntaxKind.ExtendsKeyword) { 178 | const exp = resolveExtends(node, clauseType, context); 179 | if (exp) { 180 | $extends = exp; 181 | } 182 | } 183 | 184 | // class ? implements Inherits 185 | if (clause.token === ts.SyntaxKind.ImplementsKeyword 186 | && clauseType.expression.escapedText === INHERITS_INTERFACE) { 187 | const exp = resolveInherits(node, clauseType, context); 188 | if (exp) { 189 | $inherits.push(exp); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | return { $extends, $inherits }; 197 | } 198 | 199 | function resolveDecorators(decorators: ts.NodeArray): ts.ArrayLiteralExpression { 200 | return ts.createArrayLiteral( 201 | decorators ? decorators 202 | .filter(decorator => ts.isCallExpression(decorator.expression) && ts.isIdentifier(decorator.expression.expression)) 203 | .map(decorator => (decorator.expression as ts.CallExpression).expression) 204 | : [], 205 | false); 206 | } 207 | 208 | function resolveMethod(node: ts.MethodDeclaration, context: ReflectContext) { 209 | const returns = getTypeExpression(node.type, context.args) || ts.createNull(); 210 | 211 | let params: ts.Expression[]; 212 | if (node.parameters) { 213 | params = node.parameters.map(param => { 214 | const name = ts.isIdentifier(param.name) ? param.name.escapedText.toString() : ''; 215 | if (!name) { 216 | return; 217 | } 218 | 219 | const info: AnyOf = { 220 | decorators: resolveDecorators(param.decorators), 221 | name: ts.createStringLiteral(name), 222 | type: getTypeExpression(param.type), 223 | required: param.questionToken ? ts.createFalse() : ts.createTrue(), 224 | }; 225 | return createObjectLiteral(info, true); 226 | }) 227 | .filter(v => v); 228 | } 229 | 230 | const modifiers: number[] = []; 231 | 232 | if (node.modifiers) { 233 | for (const m of node.modifiers) { 234 | switch (m.kind) { 235 | case ts.SyntaxKind.PublicKeyword: 236 | modifiers.push(TsModifier.Public); 237 | break; 238 | case ts.SyntaxKind.PrivateKeyword: 239 | modifiers.push(TsModifier.Private); 240 | break; 241 | case ts.SyntaxKind.ProtectedKeyword: 242 | modifiers.push(TsModifier.Protected); 243 | break; 244 | case ts.SyntaxKind.StaticKeyword: 245 | modifiers.push(TsModifier.Static); 246 | break; 247 | case ts.SyntaxKind.AsyncKeyword: 248 | modifiers.push(TsModifier.Async); 249 | break; 250 | } 251 | } 252 | } 253 | 254 | const type: AnyOf = { 255 | $type: ts.createNumericLiteral(TypeId.FUNCTION.toString()), 256 | $returns: returns, 257 | $decorators: resolveDecorators(node.decorators), 258 | $params: ts.createArrayLiteral(params, true), 259 | $modifiers: ts.createArrayLiteral( 260 | modifiers.map(m => ts.createNumericLiteral(m.toString())), 261 | false 262 | ), 263 | }; 264 | 265 | const comment = getComment(node, false); 266 | if (comment) { 267 | type.$description = ts.createStringLiteral(comment); 268 | } 269 | 270 | addMethodDecorator( 271 | node, 272 | EASY_METADATA, 273 | createObjectLiteral(type, true) 274 | ); 275 | 276 | return node; 277 | } 278 | -------------------------------------------------------------------------------- /packages/compiler/utils/type-parser.ts: -------------------------------------------------------------------------------- 1 | import { AnyOf, DataType, GenericArg, Nullable, ObjectType, PropType, TypeId } from '@easytype/core'; 2 | import * as ts from 'typescript'; 3 | import { createObjectLiteral } from '.'; 4 | import { ANY_ID, ARRAY_KEYWORDS, BIGINT_ID, BIGINT_NAME, BOOLEAN_ID, BOOLEAN_NAME, DATE_ID, DATE_NAME, FLOAT_ID, FLOAT_NAME, GENERIC_TYPE_ID, GENERIC_VARIABLE_ID, INTEGER_ID, INTEGER_NAME, INTERSECTION_TYPE_ID, METADATA_FIELD_NAME, NULL_ID, NUMBER_ID, NUMBER_NAME, STRING_ID, STRING_NAME, TYPEOF_TYPE_ID, TYPE_KEYWORDS, UNION_TYPE_ID } from '../constants'; 5 | import { getComment } from './comment-parser'; 6 | 7 | export const AnyType = ts.createIdentifier(ANY_ID); 8 | 9 | export function getDataType(node: ts.Node) { 10 | switch (node.kind) { 11 | case ts.SyntaxKind.StringKeyword: 12 | return DataType.STRING; 13 | case ts.SyntaxKind.NumberKeyword: 14 | return DataType.NUMBER; 15 | case ts.SyntaxKind.BooleanKeyword: 16 | return DataType.BOOLEAN; 17 | case ts.SyntaxKind.AnyKeyword: 18 | return DataType.ANY; 19 | case ts.SyntaxKind.NullKeyword: 20 | return DataType.NULL; 21 | } 22 | 23 | let type: string; 24 | if (ts.isTypeReferenceNode(node) && (ts.isIdentifier(node.typeName) || ts.isQualifiedName(node.typeName))) { 25 | type = node.typeName.getText(); 26 | } else if (ts.isIdentifier(node)) { 27 | type = node.escapedText.toString(); 28 | } 29 | 30 | if (type) { 31 | switch (type) { 32 | case STRING_NAME: 33 | return DataType.STRING; 34 | case NUMBER_NAME: 35 | return DataType.NUMBER; 36 | case BOOLEAN_NAME: 37 | return DataType.BOOLEAN; 38 | case DATE_NAME: 39 | return DataType.DATE_TIME; 40 | case INTEGER_NAME: 41 | return DataType.INTEGER; 42 | case BIGINT_NAME: 43 | return DataType.BIG_INT; 44 | case FLOAT_NAME: 45 | return DataType.FLOAT; 46 | } 47 | return DataType.OBJECT; 48 | } 49 | 50 | return null; 51 | } 52 | 53 | export function getQualifiedName(node: ts.QualifiedName): ts.Expression { 54 | let left: any = node.left; 55 | if (ts.isQualifiedName(left)) { 56 | left = getQualifiedName(left); 57 | } 58 | return ts.createPropertyAccess( 59 | left as ts.Identifier, 60 | node.right 61 | ); 62 | } 63 | 64 | export function getObjectTypeExpression(node: ts.Identifier, args: Nullable): ts.Expression { 65 | const type = node.escapedText.toString(); 66 | if (type && args && args.some(arg => arg.$name === type)) { 67 | return ts.createCall( 68 | ts.createIdentifier(GENERIC_VARIABLE_ID), 69 | undefined, 70 | [ 71 | ts.createStringLiteral(type) 72 | ] 73 | ); 74 | } 75 | return node; 76 | } 77 | 78 | export function getIndexSignatureExpression(member: ts.IndexSignatureDeclaration, args: Nullable) { 79 | const key = getTypeExpression(member.parameters[0].type); 80 | const value = getTypeExpression(member.type, args); 81 | return ts.createArrayLiteral([key, value], false); 82 | } 83 | 84 | export function getGenericArgExpression(args: Nullable) { 85 | return ts.createArrayLiteral(args.map(arg => 86 | createObjectLiteral({ 87 | ...arg, 88 | $name: ts.createStringLiteral(arg.$name), 89 | } as AnyOf) 90 | ), true); 91 | } 92 | 93 | export function getObjectType(type: ts.TypeLiteralNode | ts.ClassDeclaration, args: Nullable) { 94 | const obj: AnyOf = { 95 | $type: ts.createNumericLiteral(TypeId.OBJECT_TYPE.toString()), 96 | $properties: {} 97 | }; 98 | 99 | if (ts.isClassDeclaration(type) && type.name) { 100 | obj.$target = type.name; 101 | obj.$id = ts.createStringLiteral(type.name.text); 102 | } 103 | 104 | if (args && args.length) { 105 | obj.$args = getGenericArgExpression(args); 106 | } 107 | 108 | for (const member of type.members) { 109 | if (ts.isIndexSignatureDeclaration(member)) { 110 | obj.$index = getIndexSignatureExpression(member, args); 111 | continue; 112 | } 113 | 114 | if (!(ts.isPropertyDeclaration(member) 115 | || ts.isPropertySignature(member)) 116 | || !member.name 117 | || !ts.isIdentifier(member.name) 118 | || member.name.text === METADATA_FIELD_NAME 119 | ) { 120 | continue; 121 | } 122 | 123 | const t = getObjectPropertyType(member as any, args); 124 | if (t) { 125 | obj.$properties[member.name.text] = createObjectLiteral(t, true); 126 | } 127 | } 128 | 129 | obj.$properties = createObjectLiteral(obj.$properties, true); 130 | 131 | const comment = getComment(type); 132 | if (comment) { 133 | obj.$description = ts.createStringLiteral(comment); 134 | } 135 | 136 | return obj; 137 | } 138 | 139 | export function getTypeLiteralTypeExpression(type: ts.TypeLiteralNode | ts.ClassDeclaration, args: Nullable) { 140 | const obj = getObjectType(type, args); 141 | return createObjectLiteral(obj, true); 142 | } 143 | 144 | export function getObjectPropertyType(member: ts.PropertyDeclaration, args: Nullable): Nullable> { 145 | const type: AnyOf = { 146 | $type: ts.createNumericLiteral(TypeId.PROP_TYPE.toString()), 147 | }; 148 | 149 | if (member.modifiers) { 150 | // Ignore private property 151 | if (member.modifiers.some(modifier => 152 | modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.StaticKeyword)) { 153 | return; 154 | } 155 | 156 | if (member.modifiers.some(modifier => 157 | modifier.kind === ts.SyntaxKind.ReadonlyKeyword)) { 158 | type.$readonly = ts.createTrue(); 159 | } 160 | } 161 | 162 | if (member.questionToken) { 163 | type.$optional = ts.createTrue(); 164 | } 165 | 166 | if (member.initializer) { 167 | type.$default = member.initializer; 168 | } 169 | 170 | const comment = getComment(member); 171 | if (comment) { 172 | type.$description = ts.createStringLiteral(comment); 173 | } 174 | 175 | if (!member.type 176 | || member.type.kind === ts.SyntaxKind.UnknownKeyword 177 | || member.type.kind === ts.SyntaxKind.AnyKeyword 178 | ) { 179 | type.$ref = AnyType; 180 | return type; 181 | } 182 | 183 | const exp = getTypeExpression(member.type, args); 184 | if (exp) { 185 | type.$ref = exp; 186 | } 187 | 188 | return type; 189 | } 190 | 191 | export function getArrayExpression(type: ts.Node, args: Nullable): ts.Expression { 192 | const exp = getTypeExpression(type, args); 193 | if (!exp) { 194 | return AnyType; 195 | } 196 | return ts.createArrayLiteral( 197 | [exp], 198 | false 199 | ); 200 | } 201 | 202 | export function getGenericTypeExpression(node: ts.TypeReferenceNode, args: Nullable): ts.Expression { 203 | if (ts.isIdentifier(node.typeName)) { 204 | const text = node.typeName.escapedText.toString(); 205 | 206 | if (TYPE_KEYWORDS.includes(text)) { 207 | return getTypeExpression(node.typeArguments[0], args); 208 | } else if (ARRAY_KEYWORDS.includes(text)) { 209 | return getArrayExpression(node.typeArguments[0], args); 210 | } else { 211 | const name = node.typeName; 212 | return ts.createCall( 213 | ts.createIdentifier(GENERIC_TYPE_ID), 214 | undefined, 215 | [ 216 | ts.createBinary( 217 | name, 218 | ts.createToken(ts.SyntaxKind.BarBarToken), 219 | ts.createStringLiteral(name.text) 220 | ), 221 | ts.createArrayLiteral( 222 | node.typeArguments.map(arg => getTypeExpression(arg, args)), 223 | false 224 | ), 225 | node.pos > -1 ? ts.createStringLiteral(node.getFullText().trim()) : ts.createNull() 226 | ] 227 | ); 228 | } 229 | } 230 | } 231 | 232 | export function getMultipleTypeExpression(id: string, type: ts.UnionOrIntersectionTypeNode) { 233 | return ts.createCall( 234 | ts.createIdentifier(id), 235 | undefined, 236 | [ 237 | ts.createArrayLiteral( 238 | type.types.map(t => getTypeExpression(t as any)), 239 | false 240 | ), 241 | ] 242 | ); 243 | } 244 | 245 | export function getTypeExpression(node: ts.Node, args: Nullable = []): ts.Expression { 246 | if (!node) { 247 | return AnyType; 248 | } 249 | 250 | // a | b 251 | if (ts.isUnionTypeNode(node)) { 252 | return getMultipleTypeExpression(UNION_TYPE_ID, node); 253 | } 254 | 255 | // a & b 256 | if (ts.isIntersectionTypeNode(node)) { 257 | return getMultipleTypeExpression(INTERSECTION_TYPE_ID, node); 258 | } 259 | 260 | // a: typeof b 261 | if (ts.isTypeQueryNode(node)) { 262 | if (ts.isIdentifier(node.exprName)) { 263 | return ts.createCall( 264 | ts.createIdentifier(TYPEOF_TYPE_ID), 265 | undefined, 266 | [node.exprName] 267 | ); 268 | } 269 | return AnyType; 270 | } 271 | 272 | // Generic Type: Promise / Observable / Array / ReadonlyArray ... 273 | if (ts.isTypeReferenceNode(node) 274 | && ts.isIdentifier(node.typeName) 275 | && node.typeArguments 276 | && node.typeArguments.length 277 | ) { 278 | return getGenericTypeExpression(node, args); 279 | } 280 | 281 | // Array: var:foo[] 282 | if (ts.isArrayTypeNode(node)) { 283 | return getArrayExpression(node.elementType, args); 284 | } 285 | 286 | // Tuple: { var:[string, number] } 287 | if (ts.isTupleTypeNode(node)) { 288 | return getArrayExpression(node.elementTypes[0], args); 289 | } 290 | 291 | // obj: {a: string; ...} 292 | if (ts.isTypeLiteralNode(node)) { 293 | return getTypeLiteralTypeExpression(node, args); 294 | } 295 | 296 | const type = getDataType(node); 297 | if (type) { 298 | switch (type) { 299 | case DataType.STRING: 300 | return ts.createIdentifier(STRING_ID); 301 | case DataType.NUMBER: 302 | return ts.createIdentifier(NUMBER_ID); 303 | case DataType.BOOLEAN: 304 | return ts.createIdentifier(BOOLEAN_ID); 305 | case DataType.DATE_TIME: 306 | return ts.createIdentifier(DATE_ID); 307 | case DataType.INTEGER: 308 | return ts.createIdentifier(INTEGER_ID); 309 | case DataType.FLOAT: 310 | return ts.createIdentifier(FLOAT_ID); 311 | case DataType.BIG_INT: 312 | return ts.createIdentifier(BIGINT_ID); 313 | case DataType.OBJECT: { 314 | if (ts.isTypeReferenceNode(node)) { 315 | if (ts.isIdentifier(node.typeName)) { 316 | return getObjectTypeExpression(node.typeName, args); 317 | } 318 | // support 'ns.class' 319 | if (ts.isQualifiedName(node.typeName)) { 320 | return getQualifiedName(node.typeName); 321 | } 322 | } 323 | if (ts.isIdentifier(node)) { 324 | return getObjectTypeExpression(node, args); 325 | } 326 | } 327 | case DataType.ANY: 328 | return AnyType; 329 | case DataType.NULL: 330 | return ts.createIdentifier(NULL_ID); 331 | } 332 | } 333 | 334 | return AnyType; 335 | } 336 | -------------------------------------------------------------------------------- /packages/compiler/ts-node/index.ts: -------------------------------------------------------------------------------- 1 | import { relative, basename, extname, resolve, dirname, join } from 'path'; 2 | import sourceMapSupport = require('source-map-support'); 3 | import * as ynModule from 'yn'; 4 | import { BaseError } from 'make-error'; 5 | import * as util from 'util'; 6 | import * as _ts from 'typescript'; 7 | 8 | /** 9 | * Does this version of node obey the package.json "type" field 10 | * and throw ERR_REQUIRE_ESM when attempting to require() an ESM modules. 11 | */ 12 | const engineSupportsPackageTypeField = parseInt(process.versions.node.split('.')[0], 10) >= 12; 13 | 14 | // Loaded conditionally so we don't need to support older node versions 15 | let assertScriptCanLoadAsCJSImpl: ((filename: string) => void) | undefined; 16 | 17 | /** 18 | * Assert that script can be loaded as CommonJS when we attempt to require it. 19 | * If it should be loaded as ESM, throw ERR_REQUIRE_ESM like node does. 20 | */ 21 | function assertScriptCanLoadAsCJS(filename: string) { 22 | if (!engineSupportsPackageTypeField) { return; } 23 | if (!assertScriptCanLoadAsCJSImpl) { assertScriptCanLoadAsCJSImpl = require('../dist-raw/node-cjs-loader-utils').assertScriptCanLoadAsCJSImpl; } 24 | assertScriptCanLoadAsCJSImpl!(filename); 25 | } 26 | 27 | /** 28 | * Registered `ts-node` instance information. 29 | */ 30 | export const REGISTER_INSTANCE = Symbol.for('ts-node.register.instance'); 31 | 32 | /** 33 | * Expose `REGISTER_INSTANCE` information on node.js `process`. 34 | */ 35 | declare global { 36 | namespace NodeJS { 37 | interface Process { 38 | [REGISTER_INSTANCE]?: Register; 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * @internal 45 | */ 46 | export const INSPECT_CUSTOM = util.inspect.custom || 'inspect'; 47 | 48 | /** 49 | * Wrapper around yn module that returns `undefined` instead of `null`. 50 | * This is implemented by yn v4, but we're staying on v3 to avoid v4's node 10 requirement. 51 | */ 52 | function yn(value: string | undefined) { 53 | return ynModule(value) ?? undefined; 54 | } 55 | 56 | /** 57 | * Debugging `ts-node`. 58 | */ 59 | const shouldDebug = yn(process.env.TS_NODE_DEBUG); 60 | /** @internal */ 61 | export const debug = shouldDebug ? 62 | (...args: any) => console.log(`[ts-node ${new Date().toISOString()}]`, ...args) 63 | : () => undefined; 64 | const debugFn = shouldDebug ? 65 | (key: string, fn: (arg: T) => U) => { 66 | let i = 0; 67 | return (x: T) => { 68 | debug(key, x, ++i); 69 | return fn(x); 70 | }; 71 | } : 72 | (_: string, fn: (arg: T) => U) => fn; 73 | 74 | /** 75 | * Common TypeScript interfaces between versions. 76 | */ 77 | export interface TSCommon { 78 | version: typeof _ts.version; 79 | sys: typeof _ts.sys; 80 | ScriptSnapshot: typeof _ts.ScriptSnapshot; 81 | displayPartsToString: typeof _ts.displayPartsToString; 82 | createLanguageService: typeof _ts.createLanguageService; 83 | getDefaultLibFilePath: typeof _ts.getDefaultLibFilePath; 84 | getPreEmitDiagnostics: typeof _ts.getPreEmitDiagnostics; 85 | flattenDiagnosticMessageText: typeof _ts.flattenDiagnosticMessageText; 86 | transpileModule: typeof _ts.transpileModule; 87 | ModuleKind: typeof _ts.ModuleKind; 88 | ScriptTarget: typeof _ts.ScriptTarget; 89 | findConfigFile: typeof _ts.findConfigFile; 90 | readConfigFile: typeof _ts.readConfigFile; 91 | parseJsonConfigFileContent: typeof _ts.parseJsonConfigFileContent; 92 | formatDiagnostics: typeof _ts.formatDiagnostics; 93 | formatDiagnosticsWithColorAndContext: typeof _ts.formatDiagnosticsWithColorAndContext; 94 | } 95 | 96 | /** 97 | * Export the current version. 98 | */ 99 | export const VERSION = require('../package.json').version; 100 | 101 | /** 102 | * Options for creating a new TypeScript compiler instance. 103 | */ 104 | export interface CreateOptions { 105 | /** 106 | * Specify working directory for config resolution. 107 | * 108 | * @default process.cwd() 109 | */ 110 | dir?: string; 111 | /** 112 | * Emit output files into `.ts-node` directory. 113 | * 114 | * @default false 115 | */ 116 | emit?: boolean; 117 | /** 118 | * Scope compiler to files within `cwd`. 119 | * 120 | * @default false 121 | */ 122 | scope?: boolean; 123 | /** 124 | * Use pretty diagnostic formatter. 125 | * 126 | * @default false 127 | */ 128 | pretty?: boolean; 129 | /** 130 | * Use TypeScript's faster `transpileModule`. 131 | * 132 | * @default false 133 | */ 134 | transpileOnly?: boolean; 135 | /** 136 | * **DEPRECATED** Specify type-check is enabled (e.g. `transpileOnly == false`). 137 | * 138 | * @default true 139 | */ 140 | typeCheck?: boolean; 141 | /** 142 | * Use TypeScript's compiler host API. 143 | * 144 | * @default false 145 | */ 146 | compilerHost?: boolean; 147 | /** 148 | * Logs TypeScript errors to stderr instead of throwing exceptions. 149 | * 150 | * @default false 151 | */ 152 | logError?: boolean; 153 | /** 154 | * Load files from `tsconfig.json` on startup. 155 | * 156 | * @default false 157 | */ 158 | files?: boolean; 159 | /** 160 | * Specify a custom TypeScript compiler. 161 | * 162 | * @default "typescript" 163 | */ 164 | compiler?: string; 165 | /** 166 | * Override the path patterns to skip compilation. 167 | * 168 | * @default /node_modules/ 169 | * @docsDefault "/node_modules/" 170 | */ 171 | ignore?: string[]; 172 | /** 173 | * Path to TypeScript JSON project file. 174 | */ 175 | project?: string; 176 | /** 177 | * Skip project config resolution and loading. 178 | * 179 | * @default false 180 | */ 181 | skipProject?: boolean; 182 | /** 183 | * Skip ignore check. 184 | * 185 | * @default false 186 | */ 187 | skipIgnore?: boolean; 188 | /** 189 | * JSON object to merge with compiler options. 190 | * 191 | * @allOf [{"$ref": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json#definitions/compilerOptionsDefinition/properties/compilerOptions"}] 192 | */ 193 | compilerOptions?: object; 194 | /** 195 | * Ignore TypeScript warnings by diagnostic code. 196 | */ 197 | ignoreDiagnostics?: Array; 198 | readFile?: (path: string) => string | undefined; 199 | fileExists?: (path: string) => boolean; 200 | transformers?: _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers); 201 | /** 202 | * True if require() hooks should interop with experimental ESM loader. 203 | * Enabled explicitly via a flag since it is a breaking change. 204 | * @internal 205 | */ 206 | experimentalEsmLoader?: boolean; 207 | } 208 | 209 | /** 210 | * Options for registering a TypeScript compiler instance globally. 211 | */ 212 | export interface RegisterOptions extends CreateOptions { 213 | /** 214 | * Re-order file extensions so that TypeScript imports are preferred. 215 | * 216 | * @default false 217 | */ 218 | preferTsExts?: boolean; 219 | } 220 | 221 | /** 222 | * Must be an interface to support `typescript-json-schema`. 223 | */ 224 | export interface TsConfigOptions extends Omit { } 232 | 233 | /** 234 | * Like `Object.assign`, but ignores `undefined` properties. 235 | */ 236 | function assign(initialValue: T, ...sources: Array): T { 237 | for (const source of sources) { 238 | for (const key of Object.keys(source)) { 239 | const value = (source as any)[key]; 240 | if (value !== undefined) { (initialValue as any)[key] = value; } 241 | } 242 | } 243 | return initialValue; 244 | } 245 | 246 | /** 247 | * Information retrieved from type info check. 248 | */ 249 | export interface TypeInfo { 250 | name: string; 251 | comment: string; 252 | } 253 | 254 | /** 255 | * Default register options, including values specified via environment 256 | * variables. 257 | */ 258 | export const DEFAULTS: RegisterOptions = { 259 | dir: process.env.TS_NODE_DIR, 260 | emit: yn(process.env.TS_NODE_EMIT), 261 | scope: yn(process.env.TS_NODE_SCOPE), 262 | files: yn(process.env.TS_NODE_FILES), 263 | pretty: yn(process.env.TS_NODE_PRETTY), 264 | compiler: process.env.TS_NODE_COMPILER, 265 | compilerOptions: parse(process.env.TS_NODE_COMPILER_OPTIONS), 266 | ignore: split(process.env.TS_NODE_IGNORE), 267 | project: process.env.TS_NODE_PROJECT, 268 | skipProject: yn(process.env.TS_NODE_SKIP_PROJECT), 269 | skipIgnore: yn(process.env.TS_NODE_SKIP_IGNORE), 270 | preferTsExts: yn(process.env.TS_NODE_PREFER_TS_EXTS), 271 | ignoreDiagnostics: split(process.env.TS_NODE_IGNORE_DIAGNOSTICS), 272 | transpileOnly: yn(process.env.TS_NODE_TRANSPILE_ONLY), 273 | typeCheck: yn(process.env.TS_NODE_TYPE_CHECK), 274 | compilerHost: yn(process.env.TS_NODE_COMPILER_HOST), 275 | logError: yn(process.env.TS_NODE_LOG_ERROR), 276 | experimentalEsmLoader: false 277 | }; 278 | 279 | /** 280 | * Default TypeScript compiler options required by `ts-node`. 281 | */ 282 | const TS_NODE_COMPILER_OPTIONS = { 283 | sourceMap: true, 284 | inlineSourceMap: false, 285 | inlineSources: true, 286 | declaration: false, 287 | noEmit: false, 288 | outDir: '.ts-node' 289 | }; 290 | 291 | /** 292 | * Split a string array of values. 293 | */ 294 | export function split(value: string | undefined) { 295 | return typeof value === 'string' ? value.split(/ *, */g) : undefined; 296 | } 297 | 298 | /** 299 | * Parse a string as JSON. 300 | */ 301 | export function parse(value: string | undefined): object | undefined { 302 | return typeof value === 'string' ? JSON.parse(value) : undefined; 303 | } 304 | 305 | /** 306 | * Replace backslashes with forward slashes. 307 | */ 308 | export function normalizeSlashes(value: string): string { 309 | return value.replace(/\\/g, '/'); 310 | } 311 | 312 | /** 313 | * TypeScript diagnostics error. 314 | */ 315 | export class TSError extends BaseError { 316 | name = 'TSError'; 317 | 318 | constructor(public diagnosticText: string, public diagnosticCodes: number[]) { 319 | super(`⨯ Unable to compile TypeScript:\n${diagnosticText}`); 320 | } 321 | 322 | /** 323 | * @internal 324 | */ 325 | [INSPECT_CUSTOM]() { 326 | return this.diagnosticText; 327 | } 328 | } 329 | 330 | /** 331 | * Return type for registering `ts-node`. 332 | */ 333 | export interface Register { 334 | ts: TSCommon; 335 | config: _ts.ParsedCommandLine; 336 | options: RegisterOptions; 337 | enabled(enabled?: boolean): boolean; 338 | ignored(fileName: string): boolean; 339 | compile(code: string, fileName: string, lineOffset?: number): string; 340 | getTypeInfo(code: string, fileName: string, position: number): TypeInfo; 341 | } 342 | 343 | /** 344 | * Cached fs operation wrapper. 345 | */ 346 | function cachedLookup(fn: (arg: string) => T): (arg: string) => T { 347 | const cache = new Map(); 348 | 349 | return (arg: string): T => { 350 | if (!cache.has(arg)) { 351 | cache.set(arg, fn(arg)); 352 | } 353 | 354 | return cache.get(arg)!; 355 | }; 356 | } 357 | 358 | /** @internal */ 359 | export function getExtensions(config: _ts.ParsedCommandLine) { 360 | const tsExtensions = ['.ts']; 361 | const jsExtensions = []; 362 | 363 | // Enable additional extensions when JSX or `allowJs` is enabled. 364 | if (config.options.jsx) { tsExtensions.push('.tsx'); } 365 | if (config.options.allowJs) { jsExtensions.push('.js'); } 366 | if (config.options.jsx && config.options.allowJs) { jsExtensions.push('.jsx'); } 367 | return { tsExtensions, jsExtensions }; 368 | } 369 | 370 | /** 371 | * Register TypeScript compiler instance onto node.js 372 | */ 373 | export function register(opts: RegisterOptions = {}): Register { 374 | const originalJsHandler = require.extensions['.js'] // tslint:disable-line 375 | const service = create(opts); 376 | const { tsExtensions, jsExtensions } = getExtensions(service.config); 377 | const extensions = [...tsExtensions, ...jsExtensions]; 378 | 379 | // Expose registered instance globally. 380 | process[REGISTER_INSTANCE] = service; 381 | 382 | // Register the extensions. 383 | registerExtensions(service.options.preferTsExts, extensions, service, originalJsHandler); 384 | 385 | return service; 386 | } 387 | 388 | /** 389 | * Create TypeScript compiler instance. 390 | */ 391 | export function create(rawOptions: CreateOptions = {}): Register { 392 | const dir = rawOptions.dir ?? DEFAULTS.dir; 393 | const compilerName = rawOptions.compiler ?? DEFAULTS.compiler; 394 | const cwd = dir ? resolve(dir) : process.cwd(); 395 | 396 | /** 397 | * Load the typescript compiler. It is required to load the tsconfig but might 398 | * be changed by the tsconfig, so we sometimes have to do this twice. 399 | */ 400 | function loadCompiler(name: string | undefined) { 401 | const compiler = require.resolve(name || 'typescript', { paths: [cwd, __dirname] }); 402 | const ts: typeof _ts = require(compiler); 403 | return { compiler, ts }; 404 | } 405 | 406 | // Compute minimum options to read the config file. 407 | let { compiler, ts } = loadCompiler(compilerName); 408 | 409 | // Read config file and merge new options between env and CLI options. 410 | const { config, options: tsconfigOptions } = readConfig(cwd, ts, rawOptions); 411 | const options = assign({}, DEFAULTS, tsconfigOptions || {}, rawOptions); 412 | 413 | // If `compiler` option changed based on tsconfig, re-load the compiler. 414 | if (options.compiler !== compilerName) { 415 | ({ compiler, ts } = loadCompiler(options.compiler)); 416 | } 417 | 418 | const readFile = options.readFile || ts.sys.readFile; 419 | const fileExists = options.fileExists || ts.sys.fileExists; 420 | const transpileOnly = options.transpileOnly === true || options.typeCheck === false; 421 | const transformers = options.transformers || undefined; 422 | const ignoreDiagnostics = [ 423 | 6059, // "'rootDir' is expected to contain all source files." 424 | 18002, // "The 'files' list in config file is empty." 425 | 18003, // "No inputs were found in config file." 426 | ...(options.ignoreDiagnostics || []) 427 | ].map(Number); 428 | 429 | const configDiagnosticList = filterDiagnostics(config.errors, ignoreDiagnostics); 430 | const outputCache = new Map(); 433 | 434 | const isScoped = options.scope ? (relname: string) => relname.charAt(0) !== '.' : () => true; 435 | const shouldIgnore = createIgnore(options.skipIgnore ? [] : ( 436 | options.ignore || ['(?:^|/)node_modules/'] 437 | ).map(str => new RegExp(str))); 438 | 439 | const diagnosticHost: _ts.FormatDiagnosticsHost = { 440 | getNewLine: () => ts.sys.newLine, 441 | getCurrentDirectory: () => cwd, 442 | getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? x => x : x => x.toLowerCase() 443 | }; 444 | 445 | // Install source map support and read from memory cache. 446 | sourceMapSupport.install({ 447 | environment: 'node', 448 | retrieveFile(path: string) { 449 | return outputCache.get(normalizeSlashes(path))?.content || ''; 450 | } 451 | }); 452 | 453 | const formatDiagnostics = process.stdout.isTTY || options.pretty 454 | ? (ts.formatDiagnosticsWithColorAndContext || ts.formatDiagnostics) 455 | : ts.formatDiagnostics; 456 | 457 | function createTSError(diagnostics: ReadonlyArray<_ts.Diagnostic>) { 458 | const diagnosticText = formatDiagnostics(diagnostics, diagnosticHost); 459 | const diagnosticCodes = diagnostics.map(x => x.code); 460 | return new TSError(diagnosticText, diagnosticCodes); 461 | } 462 | 463 | function reportTSError(configDiagnosticList: _ts.Diagnostic[]) { 464 | const error = createTSError(configDiagnosticList); 465 | if (options.logError) { 466 | // Print error in red color and continue execution. 467 | console.error('\x1b[31m%s\x1b[0m', error); 468 | } else { 469 | // Throw error and exit the script. 470 | throw error; 471 | } 472 | } 473 | 474 | // Render the configuration errors. 475 | if (configDiagnosticList.length) { reportTSError(configDiagnosticList); } 476 | 477 | /** 478 | * Get the extension for a transpiled file. 479 | */ 480 | const getExtension = config.options.jsx === ts.JsxEmit.Preserve ? 481 | ((path: string) => /\.[tj]sx$/.test(path) ? '.jsx' : '.js') : 482 | ((_: string) => '.js'); 483 | 484 | /** 485 | * Create the basic required function using transpile mode. 486 | */ 487 | let getOutput: (code: string, fileName: string) => SourceOutput; 488 | let getTypeInfo: (_code: string, _fileName: string, _position: number) => TypeInfo; 489 | 490 | // Use full language services when the fast option is disabled. 491 | if (!transpileOnly) { 492 | const fileContents = new Map(); 493 | const rootFileNames = new Set(config.fileNames); 494 | const cachedReadFile = cachedLookup(debugFn('readFile', readFile)); 495 | 496 | // Use language services by default (TODO: invert next major version). 497 | if (!options.compilerHost) { 498 | let projectVersion = 1; 499 | const fileVersions = new Map(Array.from(rootFileNames).map(fileName => [fileName, 0])); 500 | 501 | const getCustomTransformers = () => { 502 | if (typeof transformers === 'function') { 503 | const program = service.getProgram(); 504 | return program ? transformers(program) : undefined; 505 | } 506 | 507 | return transformers; 508 | }; 509 | 510 | // Create the compiler host for type checking. 511 | const serviceHost: _ts.LanguageServiceHost = { 512 | getProjectVersion: () => String(projectVersion), 513 | getScriptFileNames: () => Array.from(rootFileNames), 514 | getScriptVersion: (fileName: string) => { 515 | const version = fileVersions.get(fileName); 516 | return version ? version.toString() : ''; 517 | }, 518 | getScriptSnapshot(fileName: string) { 519 | let contents = fileContents.get(fileName); 520 | 521 | // Read contents into TypeScript memory cache. 522 | if (contents === undefined) { 523 | contents = cachedReadFile(fileName); 524 | if (contents === undefined) { return; } 525 | 526 | fileVersions.set(fileName, 1); 527 | fileContents.set(fileName, contents); 528 | projectVersion++; 529 | } 530 | 531 | return ts.ScriptSnapshot.fromString(contents); 532 | }, 533 | readFile: cachedReadFile, 534 | readDirectory: ts.sys.readDirectory, 535 | getDirectories: cachedLookup(debugFn('getDirectories', ts.sys.getDirectories)), 536 | fileExists: cachedLookup(debugFn('fileExists', fileExists)), 537 | directoryExists: cachedLookup(debugFn('directoryExists', ts.sys.directoryExists)), 538 | getNewLine: () => ts.sys.newLine, 539 | useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, 540 | getCurrentDirectory: () => cwd, 541 | getCompilationSettings: () => config.options, 542 | getDefaultLibFileName: () => ts.getDefaultLibFilePath(config.options), 543 | getCustomTransformers 544 | }; 545 | 546 | const registry = ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, cwd); 547 | const service = ts.createLanguageService(serviceHost, registry); 548 | 549 | const updateMemoryCache = (contents: string, fileName: string) => { 550 | // Add to `rootFiles` if not already there 551 | // This is necessary to force TS to emit output 552 | if (!rootFileNames.has(fileName)) { 553 | rootFileNames.add(fileName); 554 | // Increment project version for every change to rootFileNames. 555 | projectVersion++; 556 | } 557 | 558 | const previousVersion = fileVersions.get(fileName) || 0; 559 | const previousContents = fileContents.get(fileName); 560 | // Avoid incrementing cache when nothing has changed. 561 | if (contents !== previousContents) { 562 | fileVersions.set(fileName, previousVersion + 1); 563 | fileContents.set(fileName, contents); 564 | // Increment project version for every file change. 565 | projectVersion++; 566 | } 567 | }; 568 | 569 | let previousProgram: _ts.Program | undefined; 570 | 571 | getOutput = (code: string, fileName: string) => { 572 | updateMemoryCache(code, fileName); 573 | 574 | const programBefore = service.getProgram(); 575 | if (programBefore !== previousProgram) { 576 | debug(`compiler rebuilt Program instance when getting output for ${fileName}`); 577 | } 578 | 579 | const output = service.getEmitOutput(fileName); 580 | 581 | // Get the relevant diagnostics - this is 3x faster than `getPreEmitDiagnostics`. 582 | const diagnostics = service.getSemanticDiagnostics(fileName) 583 | .concat(service.getSyntacticDiagnostics(fileName)); 584 | 585 | const programAfter = service.getProgram(); 586 | 587 | debug( 588 | 'invariant: Is service.getProject() identical before and after getting emit output and diagnostics? (should always be true) ', 589 | programBefore === programAfter 590 | ); 591 | 592 | previousProgram = programAfter; 593 | 594 | const diagnosticList = filterDiagnostics(diagnostics, ignoreDiagnostics); 595 | if (diagnosticList.length) { reportTSError(diagnosticList); } 596 | 597 | if (output.emitSkipped) { 598 | throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`); 599 | } 600 | 601 | // Throw an error when requiring `.d.ts` files. 602 | if (output.outputFiles.length === 0) { 603 | throw new TypeError( 604 | `Unable to require file: ${relative(cwd, fileName)}\n` + 605 | 'This is usually the result of a faulty configuration or import. ' + 606 | 'Make sure there is a `.js`, `.json` or other executable extension with ' + 607 | 'loader attached before `ts-node` available.' 608 | ); 609 | } 610 | 611 | return [output.outputFiles[1].text, output.outputFiles[0].text]; 612 | }; 613 | 614 | getTypeInfo = (code: string, fileName: string, position: number) => { 615 | updateMemoryCache(code, fileName); 616 | 617 | const info = service.getQuickInfoAtPosition(fileName, position); 618 | const name = ts.displayPartsToString(info ? info.displayParts : []); 619 | const comment = ts.displayPartsToString(info ? info.documentation : []); 620 | 621 | return { name, comment }; 622 | }; 623 | } else { 624 | const sys = { 625 | ...ts.sys, 626 | ...diagnosticHost, 627 | readFile: (fileName: string) => { 628 | const cacheContents = fileContents.get(fileName); 629 | if (cacheContents !== undefined) { return cacheContents; } 630 | return cachedReadFile(fileName); 631 | }, 632 | readDirectory: ts.sys.readDirectory, 633 | getDirectories: cachedLookup(debugFn('getDirectories', ts.sys.getDirectories)), 634 | fileExists: cachedLookup(debugFn('fileExists', fileExists)), 635 | directoryExists: cachedLookup(debugFn('directoryExists', ts.sys.directoryExists)), 636 | resolvePath: cachedLookup(debugFn('resolvePath', ts.sys.resolvePath)), 637 | realpath: ts.sys.realpath ? cachedLookup(debugFn('realpath', ts.sys.realpath)) : undefined 638 | }; 639 | 640 | const host: _ts.CompilerHost = ts.createIncrementalCompilerHost 641 | ? ts.createIncrementalCompilerHost(config.options, sys) 642 | : { 643 | ...sys, 644 | getSourceFile: (fileName, languageVersion) => { 645 | const contents = sys.readFile(fileName); 646 | if (contents === undefined) { return; } 647 | return ts.createSourceFile(fileName, contents, languageVersion); 648 | }, 649 | getDefaultLibLocation: () => normalizeSlashes(dirname(compiler)), 650 | getDefaultLibFileName: () => normalizeSlashes(join(dirname(compiler), ts.getDefaultLibFileName(config.options))), 651 | useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames 652 | }; 653 | 654 | // Fallback for older TypeScript releases without incremental API. 655 | let builderProgram = ts.createIncrementalProgram 656 | ? ts.createIncrementalProgram({ 657 | rootNames: Array.from(rootFileNames), 658 | options: config.options, 659 | host, 660 | configFileParsingDiagnostics: config.errors, 661 | projectReferences: config.projectReferences 662 | }) 663 | : ts.createEmitAndSemanticDiagnosticsBuilderProgram( 664 | Array.from(rootFileNames), 665 | config.options, 666 | host, 667 | undefined, 668 | config.errors, 669 | config.projectReferences 670 | ); 671 | 672 | // Read and cache custom transformers. 673 | const customTransformers = typeof transformers === 'function' 674 | ? transformers(builderProgram.getProgram()) 675 | : transformers; 676 | 677 | // Set the file contents into cache manually. 678 | const updateMemoryCache = (contents: string, fileName: string) => { 679 | const sourceFile = builderProgram.getSourceFile(fileName); 680 | 681 | fileContents.set(fileName, contents); 682 | 683 | // Add to `rootFiles` when discovered by compiler for the first time. 684 | if (sourceFile === undefined) { 685 | rootFileNames.add(fileName); 686 | } 687 | 688 | // Update program when file changes. 689 | if (sourceFile === undefined || sourceFile.text !== contents) { 690 | builderProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram( 691 | Array.from(rootFileNames), 692 | config.options, 693 | host, 694 | builderProgram, 695 | config.errors, 696 | config.projectReferences 697 | ); 698 | } 699 | }; 700 | 701 | getOutput = (code: string, fileName: string) => { 702 | const output: [string, string] = ['', '']; 703 | 704 | updateMemoryCache(code, fileName); 705 | 706 | const sourceFile = builderProgram.getSourceFile(fileName); 707 | if (!sourceFile) { throw new TypeError(`Unable to read file: ${fileName}`); } 708 | 709 | const program = builderProgram.getProgram(); 710 | const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); 711 | const diagnosticList = filterDiagnostics(diagnostics, ignoreDiagnostics); 712 | if (diagnosticList.length) { reportTSError(diagnosticList); } 713 | 714 | const result = builderProgram.emit(sourceFile, (path, file, writeByteOrderMark) => { 715 | if (path.endsWith('.map')) { 716 | output[1] = file; 717 | } else { 718 | output[0] = file; 719 | } 720 | 721 | if (options.emit) { sys.writeFile(path, file, writeByteOrderMark); } 722 | }, undefined, undefined, customTransformers); 723 | 724 | if (result.emitSkipped) { 725 | throw new TypeError(`${relative(cwd, fileName)}: Emit skipped`); 726 | } 727 | 728 | // Throw an error when requiring files that cannot be compiled. 729 | if (output[0] === '') { 730 | if (program.isSourceFileFromExternalLibrary(sourceFile)) { 731 | throw new TypeError(`Unable to compile file from external library: ${relative(cwd, fileName)}`); 732 | } 733 | 734 | throw new TypeError( 735 | `Unable to require file: ${relative(cwd, fileName)}\n` + 736 | 'This is usually the result of a faulty configuration or import. ' + 737 | 'Make sure there is a `.js`, `.json` or other executable extension with ' + 738 | 'loader attached before `ts-node` available.' 739 | ); 740 | } 741 | 742 | return output; 743 | }; 744 | 745 | getTypeInfo = (code: string, fileName: string, position: number) => { 746 | updateMemoryCache(code, fileName); 747 | 748 | const sourceFile = builderProgram.getSourceFile(fileName); 749 | if (!sourceFile) { throw new TypeError(`Unable to read file: ${fileName}`); } 750 | 751 | const node = getTokenAtPosition(ts, sourceFile, position); 752 | const checker = builderProgram.getProgram().getTypeChecker(); 753 | const symbol = checker.getSymbolAtLocation(node); 754 | 755 | if (!symbol) { return { name: '', comment: '' }; } 756 | 757 | const type = checker.getTypeOfSymbolAtLocation(symbol, node); 758 | const signatures = [...type.getConstructSignatures(), ...type.getCallSignatures()]; 759 | 760 | return { 761 | name: signatures.length ? signatures.map(x => checker.signatureToString(x)).join('\n') : checker.typeToString(type), 762 | comment: ts.displayPartsToString(symbol ? symbol.getDocumentationComment(checker) : []) 763 | }; 764 | }; 765 | 766 | // Write `.tsbuildinfo` when `--build` is enabled. 767 | if (options.emit && config.options.incremental) { 768 | process.on('exit', () => { 769 | // Emits `.tsbuildinfo` to filesystem. 770 | (builderProgram.getProgram() as any).emitBuildInfo(); 771 | }); 772 | } 773 | } 774 | } else { 775 | if (typeof transformers === 'function') { 776 | throw new TypeError('Transformers function is unavailable in "--transpile-only"'); 777 | } 778 | 779 | getOutput = (code: string, fileName: string): SourceOutput => { 780 | const result = ts.transpileModule(code, { 781 | fileName, 782 | compilerOptions: config.options, 783 | reportDiagnostics: true, 784 | transformers 785 | }); 786 | 787 | const diagnosticList = filterDiagnostics(result.diagnostics || [], ignoreDiagnostics); 788 | if (diagnosticList.length) { reportTSError(diagnosticList); } 789 | 790 | return [result.outputText, result.sourceMapText as string]; 791 | }; 792 | 793 | getTypeInfo = () => { 794 | throw new TypeError('Type information is unavailable in "--transpile-only"'); 795 | }; 796 | } 797 | 798 | // Create a simple TypeScript compiler proxy. 799 | function compile(code: string, fileName: string, lineOffset = 0) { 800 | const normalizedFileName = normalizeSlashes(fileName); 801 | const [value, sourceMap] = getOutput(code, normalizedFileName); 802 | const output = updateOutput(value, normalizedFileName, sourceMap, getExtension); 803 | outputCache.set(normalizedFileName, { content: output }); 804 | return output; 805 | } 806 | 807 | let active = true; 808 | const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled); 809 | const ignored = (fileName: string) => { 810 | if (!active) { return true; } 811 | const relname = relative(cwd, fileName); 812 | if (!config.options.allowJs) { 813 | const ext = extname(fileName); 814 | if (ext === '.js' || ext === '.jsx') { return true; } 815 | } 816 | return !isScoped(relname) || shouldIgnore(relname); 817 | }; 818 | 819 | return { ts, config, compile, getTypeInfo, ignored, enabled, options }; 820 | } 821 | 822 | /** 823 | * Check if the filename should be ignored. 824 | */ 825 | function createIgnore(ignore: RegExp[]) { 826 | return (relname: string) => { 827 | const path = normalizeSlashes(relname); 828 | 829 | return ignore.some(x => x.test(path)); 830 | }; 831 | } 832 | 833 | /** 834 | * "Refreshes" an extension on `require.extensions`. 835 | * 836 | * @param {string} ext 837 | */ 838 | function reorderRequireExtension(ext: string) { 839 | const old = require.extensions[ext] // tslint:disable-line 840 | delete require.extensions[ext] // tslint:disable-line 841 | require.extensions[ext] = old // tslint:disable-line 842 | } 843 | 844 | /** 845 | * Register the extensions to support when importing files. 846 | */ 847 | function registerExtensions( 848 | preferTsExts: boolean | null | undefined, 849 | extensions: string[], 850 | register: Register, 851 | originalJsHandler: (m: NodeModule, filename: string) => any 852 | ) { 853 | // Register new extensions. 854 | for (const ext of extensions) { 855 | registerExtension(ext, register, originalJsHandler); 856 | } 857 | 858 | if (preferTsExts) { 859 | // tslint:disable-next-line 860 | const preferredExtensions = new Set([...extensions, ...Object.keys(require.extensions)]) 861 | 862 | for (const ext of preferredExtensions) { reorderRequireExtension(ext); } 863 | } 864 | } 865 | 866 | /** 867 | * Register the extension for node. 868 | */ 869 | function registerExtension( 870 | ext: string, 871 | register: Register, 872 | originalHandler: (m: NodeModule, filename: string) => any 873 | ) { 874 | const old = require.extensions[ext] || originalHandler // tslint:disable-line 875 | 876 | require.extensions[ext] = function (m: any, filename) { // tslint:disable-line 877 | if (register.ignored(filename)) { return old(m, filename); } 878 | 879 | if (register.options.experimentalEsmLoader) { 880 | assertScriptCanLoadAsCJS(filename); 881 | } 882 | 883 | const _compile = m._compile; 884 | 885 | m._compile = function (code: string, fileName: string) { 886 | debug('module._compile', fileName); 887 | 888 | return _compile.call(this, register.compile(code, fileName), fileName); 889 | }; 890 | 891 | return old(m, filename); 892 | }; 893 | } 894 | 895 | /** 896 | * Do post-processing on config options to support `ts-node`. 897 | */ 898 | function fixConfig(ts: TSCommon, config: _ts.ParsedCommandLine) { 899 | // Delete options that *should not* be passed through. 900 | delete config.options.out; 901 | delete config.options.outFile; 902 | delete config.options.composite; 903 | delete config.options.declarationDir; 904 | delete config.options.declarationMap; 905 | delete config.options.emitDeclarationOnly; 906 | 907 | // Target ES5 output by default (instead of ES3). 908 | if (config.options.target === undefined) { 909 | config.options.target = ts.ScriptTarget.ES5; 910 | } 911 | 912 | // Target CommonJS modules by default (instead of magically switching to ES6 when the target is ES6). 913 | if (config.options.module === undefined) { 914 | config.options.module = ts.ModuleKind.CommonJS; 915 | } 916 | 917 | return config; 918 | } 919 | 920 | /** 921 | * Load TypeScript configuration. Returns the parsed TypeScript config and 922 | * any `ts-node` options specified in the config file. 923 | */ 924 | function readConfig( 925 | cwd: string, 926 | ts: TSCommon, 927 | rawOptions: CreateOptions 928 | ): { 929 | // Parsed TypeScript configuration. 930 | config: _ts.ParsedCommandLine 931 | // Options pulled from `tsconfig.json`. 932 | options: TsConfigOptions 933 | } { 934 | let config: any = { compilerOptions: {} }; 935 | let basePath = cwd; 936 | let configFileName: string | undefined; 937 | 938 | const { 939 | fileExists = ts.sys.fileExists, 940 | readFile = ts.sys.readFile, 941 | skipProject = DEFAULTS.skipProject, 942 | project = DEFAULTS.project 943 | } = rawOptions; 944 | 945 | // Read project configuration when available. 946 | if (!skipProject) { 947 | configFileName = project 948 | ? resolve(cwd, project) 949 | : ts.findConfigFile(cwd, fileExists); 950 | 951 | if (configFileName) { 952 | const result = ts.readConfigFile(configFileName, readFile); 953 | 954 | // Return diagnostics. 955 | if (result.error) { 956 | return { 957 | config: { errors: [result.error], fileNames: [], options: {} }, 958 | options: {} 959 | }; 960 | } 961 | 962 | config = result.config; 963 | basePath = dirname(configFileName); 964 | } 965 | } 966 | 967 | // Fix ts-node options that come from tsconfig.json 968 | const tsconfigOptions: TsConfigOptions = Object.assign({}, config['ts-node']); 969 | 970 | // Remove resolution of "files". 971 | const files = rawOptions.files ?? tsconfigOptions.files ?? DEFAULTS.files; 972 | if (!files) { 973 | config.files = []; 974 | config.include = []; 975 | } 976 | 977 | // Override default configuration options `ts-node` requires. 978 | config.compilerOptions = Object.assign( 979 | {}, 980 | config.compilerOptions, 981 | DEFAULTS.compilerOptions, 982 | tsconfigOptions.compilerOptions, 983 | rawOptions.compilerOptions, 984 | TS_NODE_COMPILER_OPTIONS 985 | ); 986 | 987 | const fixedConfig = fixConfig(ts, ts.parseJsonConfigFileContent(config, { 988 | fileExists, 989 | readFile, 990 | readDirectory: ts.sys.readDirectory, 991 | useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames 992 | }, basePath, undefined, configFileName)); 993 | 994 | return { config: fixedConfig, options: tsconfigOptions }; 995 | } 996 | 997 | /** 998 | * Internal source output. 999 | */ 1000 | type SourceOutput = [string, string]; 1001 | 1002 | /** 1003 | * Update the output remapping the source map. 1004 | */ 1005 | function updateOutput(outputText: string, fileName: string, sourceMap: string, getExtension: (fileName: string) => string) { 1006 | const base64Map = Buffer.from(updateSourceMap(sourceMap, fileName), 'utf8').toString('base64'); 1007 | const sourceMapContent = `data:application/json;charset=utf-8;base64,${base64Map}`; 1008 | const sourceMapLength = `${basename(fileName)}.map`.length + (getExtension(fileName).length - extname(fileName).length); 1009 | 1010 | return outputText.slice(0, -sourceMapLength) + sourceMapContent; 1011 | } 1012 | 1013 | /** 1014 | * Update the source map contents for improved output. 1015 | */ 1016 | function updateSourceMap(sourceMapText: string, fileName: string) { 1017 | const sourceMap = JSON.parse(sourceMapText); 1018 | sourceMap.file = fileName; 1019 | sourceMap.sources = [fileName]; 1020 | delete sourceMap.sourceRoot; 1021 | return JSON.stringify(sourceMap); 1022 | } 1023 | 1024 | /** 1025 | * Filter diagnostics. 1026 | */ 1027 | function filterDiagnostics(diagnostics: readonly _ts.Diagnostic[], ignore: number[]) { 1028 | return diagnostics.filter(x => ignore.indexOf(x.code) === -1); 1029 | } 1030 | 1031 | /** 1032 | * Get token at file position. 1033 | * 1034 | * Reference: https://github.com/microsoft/TypeScript/blob/fcd9334f57d85b73dd66ad2d21c02e84822f4841/src/services/utilities.ts#L705-L731 1035 | */ 1036 | function getTokenAtPosition(ts: typeof _ts, sourceFile: _ts.SourceFile, position: number): _ts.Node { 1037 | let current: _ts.Node = sourceFile; 1038 | 1039 | outer: while (true) { 1040 | for (const child of current.getChildren(sourceFile)) { 1041 | const start = child.getFullStart(); 1042 | if (start > position) { break; } 1043 | 1044 | const end = child.getEnd(); 1045 | if (position <= end) { 1046 | current = child; 1047 | continue outer; 1048 | } 1049 | } 1050 | 1051 | return current; 1052 | } 1053 | } 1054 | --------------------------------------------------------------------------------