Pull request: beta client squashed

Merge in DNS/adguard-home from beta-client-2 to master

Squashed commit of the following:

commit b2640cc49a6c5484d730b534dcf5a8013d7fa478
Merge: 659def862 aef4659e9
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 19:23:09 2020 +0300

    Merge branch 'master' into beta-client-2

commit 659def8626467949c35b7a6a0c99ffafb07b4385
Author: Eugene Burkov <e.burkov@adguard.com>
Date:   Tue Dec 29 17:25:14 2020 +0300

    all: upgrade github actions node version

commit b4b8cf8dd75672e9155da5d111ac66e8f5ba1535
Author: Vladislav Abdulmyanov <v.abdulmyanov@adguard.com>
Date:   Tue Dec 29 16:57:14 2020 +0300

    all: beta client squashed
This commit is contained in:
Eugene Burkov
2020-12-29 19:53:56 +03:00
parent aef4659e93
commit 5e20ac7ed5
200 changed files with 20843 additions and 55 deletions

12
client2/scripts/consts.ts Normal file
View File

@@ -0,0 +1,12 @@
export const OPEN_API_PATH = '../openapi/openapi.yaml';
export const ENT_DIR = './src/lib/entities';
export const API_DIR = './src/lib/apis';
export const LOCALE_FOLDER_PATH = './src/lib/intl/__locales';
export const TRANSLATOR_CLASS_NAME = 'Translator';
export const USE_INTL_NAME = 'useIntl';
export const trimQuotes = (str: string) => {
return str.replace(/\'|\"/g, '');
};
export const GENERATOR_ENTITY_ALLIAS = 'Entities/';

View File

@@ -0,0 +1,18 @@
import * as fs from 'fs';
import * as YAML from 'yaml';
import { OPEN_API_PATH } from '../consts';
import EntitiesGenerator from './src/generateEntities';
import ApisGenerator from './src/generateApis';
const generateApi = (openApi: Record<string, any>) => {
const ent = new EntitiesGenerator(openApi);
ent.save();
const api = new ApisGenerator(openApi);
api.save();
}
const openApiFile = fs.readFileSync(OPEN_API_PATH, 'utf8');
generateApi(YAML.parse(openApiFile));

View File

@@ -0,0 +1,317 @@
/* eslint-disable no-template-curly-in-string */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import * as fs from 'fs';
import * as path from 'path';
import { stringify } from 'qs';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';
import {
API_DIR as API_DIR_CONST,
GENERATOR_ENTITY_ALLIAS,
} from '../../consts';
import { toCamel, capitalize, schemaParamParser } from './utils';
const API_DIR = path.resolve(API_DIR_CONST);
if (!fs.existsSync(API_DIR)) {
fs.mkdirSync(API_DIR);
}
const { Project, QuoteKind } = morph;
class ApiGenerator {
project = new Project({
tsConfigFilePath: './tsconfig.json',
addFilesFromTsConfig: false,
manipulationSettings: {
quoteKind: QuoteKind.Single,
usePrefixAndSuffixTextForRename: false,
useTrailingCommas: true,
},
});
openapi: Record<string, any>;
serverUrl: string;
paths: any;
/* interface Controllers {
[controller: string]: {
[operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
}
} */
controllers: Record<string, any> = {};
apis: morph.SourceFile[] = [];
constructor(openapi: Record<string, any>) {
this.openapi = openapi;
this.paths = openapi.paths;
this.serverUrl = openapi.servers[0].url;
Object.keys(this.paths).forEach((pathKey) => {
Object.keys(this.paths[pathKey]).forEach((method) => {
const {
tags, operationId, parameters, responses, requestBody, security,
} = this.paths[pathKey][method];
const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));
if (this.controllers[controller]) {
this.controllers[controller][operationId] = {
parameters,
responses,
method,
requestBody,
security,
pathKey: pathKey.replace(/{/g, '${'),
};
} else {
this.controllers[controller] = { [operationId]: {
parameters,
responses,
method,
requestBody,
security,
pathKey: pathKey.replace(/{/g, '${'),
} };
}
});
});
this.generateApiFiles();
}
generateApiFiles = () => {
Object.keys(this.controllers).forEach(this.generateApiFile);
};
generateApiFile = (cName: string) => {
const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
apiFile.addStatements([
'// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'',
]);
// const schemaProperties = schemas[schemaName].properties;
const importEntities: any[] = [];
// add api class to file
const apiClass = apiFile.addClass({
name: `${capitalize(cName)}Api`,
isDefaultExport: true,
});
// get operations of controller
const controllerOperations = this.controllers[cName];
const operationList = Object.keys(controllerOperations).sort();
// for each operation add fetcher
operationList.forEach((operation) => {
const {
requestBody, responses, parameters, method, pathKey, security,
} = controllerOperations[operation];
const queryParams: any[] = []; // { name, type }
const bodyParam: any[] = []; // { name, type }
let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
let contentType = '';
if (parameters) {
parameters.forEach((p: any) => {
const [
pType, isArray, isClass, isImport,
] = schemaParamParser(p.schema, this.openapi);
if (isImport) {
importEntities.push({ type: pType, isClass });
}
if (p.in === 'query') {
queryParams.push({
name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
}
});
}
if (queryParams.length > 0) {
const imp = apiFile.getImportDeclaration((i) => {
return i.getModuleSpecifierValue() === 'qs';
}); if (!imp) {
apiFile.addImportDeclaration({
moduleSpecifier: 'qs',
defaultImport: 'qs',
});
}
}
if (requestBody) {
let content = requestBody.content;
const { $ref }: { $ref: string } = requestBody;
if (!content && $ref) {
const name = $ref.split('/').pop() as string;
content = this.openapi.components.requestBodies[name].content;
}
[contentType] = Object.keys(content);
const data = content[contentType];
const [
pType, isArray, isClass, isImport,
] = schemaParamParser(data.schema, this.openapi);
if (isImport) {
importEntities.push({ type: pType, isClass });
bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
} else {
bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
}
}
if (responses['200']) {
const { content, headers } = responses['200'];
if (content && (content['*/*'] || content['application/json'])) {
const { schema, examples } = content['*/*'] || content['application/json'];
if (!schema) {
process.exit(0);
}
const propType = schemaParamParser(schema, this.openapi);
const [pType, , isClass, isImport] = propType;
if (isImport) {
importEntities.push({ type: pType, isClass });
}
hasResponseBodyType = propType;
}
}
let returnType = '';
if (hasResponseBodyType) {
const [pType, isArray, isClass] = hasResponseBodyType as any;
let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
returnType = data;
} else {
returnType = 'Promise<number';
}
const shouldValidate = bodyParam.filter(b => b.isClass);
if (shouldValidate.length > 0) {
returnType += ' | string[]';
}
// append Error to default type return;
returnType += ' | Error>';
const fetcher = apiClass.addMethod({
isAsync: true,
isStatic: true,
name: operation,
returnType,
});
const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
fetcher.addParameters(params);
fetcher.setBodyText((w) => {
// Add data to URLSearchParams
if (contentType === 'text/plain') {
bodyParam.forEach((b) => {
w.writeLine(`const params = String(${b.name});`);
});
} else {
if (shouldValidate.length > 0) {
w.writeLine(`const haveError: string[] = [];`);
shouldValidate.forEach((b) => {
w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
});
w.writeLine(`if (haveError.length > 0) {`);
w.writeLine(` return Promise.resolve(haveError);`)
w.writeLine(`}`);
}
}
// Switch return of fetch in case on queryParams
if (queryParams.length > 0) {
w.writeLine('const queryParams = {');
queryParams.forEach((q) => {
w.writeLine(` ${q.name}: ${q.name},`);
});
w.writeLine('}');
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
} else {
w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
}
// Add method
w.writeLine(` method: '${method.toUpperCase()}',`);
// add Fetch options
if (contentType && contentType !== 'multipart/form-data') {
w.writeLine(' headers: {');
w.writeLine(` 'Content-Type': '${contentType}',`);
w.writeLine(' },');
}
if (contentType) {
switch (contentType) {
case 'text/plain':
w.writeLine(' body: params,');
break;
default:
w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
break;
}
}
// Handle response
if (hasResponseBodyType) {
w.writeLine('}).then(async (res) => {');
w.writeLine(' if (res.status === 200) {');
w.writeLine(' return res.json();');
} else {
w.writeLine('}).then(async (res) => {');
w.writeLine(' if (res.status === 200) {');
w.writeLine(' return res.status;');
}
// Handle Error
w.writeLine(' } else {');
w.writeLine(' return new Error(String(res.status));');
w.writeLine(' }');
w.writeLine('})');
});
});
const imports: any[] = [];
const types: string[] = [];
importEntities.forEach((i) => {
const { type } = i;
if (!types.includes(type)) {
imports.push(i);
types.push(type);
}
});
imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie;
if (isClass) {
apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
defaultImport: pType,
namedImports: [`I${pType}`],
});
} else {
apiFile.addImportDeclaration({
moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
namedImports: [pType],
});
}
});
this.apis.push(apiFile);
};
save = () => {
this.apis.forEach(async (e) => {
await e.saveSync();
});
};
}
export default ApiGenerator;

View File

@@ -0,0 +1,519 @@
import * as fs from 'fs';
import * as path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';
import { ENT_DIR } from '../../consts';
import { TYPES, toCamel, schemaParamParser } from './utils';
const { Project, QuoteKind } = morph;
const EntDir = path.resolve(ENT_DIR);
if (!fs.existsSync(EntDir)) {
fs.mkdirSync(EntDir);
}
class EntitiesGenerator {
project = new Project({
tsConfigFilePath: './tsconfig.json',
addFilesFromTsConfig: false,
manipulationSettings: {
quoteKind: QuoteKind.Single,
usePrefixAndSuffixTextForRename: false,
useTrailingCommas: true,
},
});
openapi: Record<string, any>;
schemas: Record<string, any>;
schemaNames: string[];
entities: morph.SourceFile[] = [];
constructor(openapi: Record<string, any>) {
this.openapi = openapi;
this.schemas = openapi.components.schemas;
this.schemaNames = Object.keys(this.schemas);
this.generateEntities();
}
generateEntities = () => {
this.schemaNames.forEach(this.generateEntity);
};
generateEntity = (sName: string) => {
const { properties, type, oneOf } = this.schemas[sName];
const notAClass = !properties && TYPES[type as keyof typeof TYPES];
if (oneOf) {
this.generateOneOf(sName);
return;
}
if (notAClass) {
this.generateEnum(sName);
} else {
this.generateClass(sName);
}
};
generateEnum = (sName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
entityFile.addStatements([
'// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'',
]);
const { enum: enumMembers } = this.schemas[sName];
entityFile.addEnum({
name: sName,
members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
isExported: true,
});
this.entities.push(entityFile);
};
generateOneOf = (sName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
entityFile.addStatements([
'// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'',
]);
const importEntities: { type: string, isClass: boolean }[] = [];
const entities = this.schemas[sName].oneOf.map((elem: any) => {
const [
pType, isArray, isClass, isImport,
] = schemaParamParser(elem, this.openapi);
importEntities.push({ type: pType, isClass });
return { type: pType, isArray };
});
entityFile.addTypeAlias({
name: sName,
isExported: true,
type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
})
// add import
importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie;
if (isClass) {
entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`,
namedImports: [`I${pType}`],
});
} else {
entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`,
namedImports: [pType],
});
}
});
this.entities.push(entityFile);
}
generateClass = (sName: string) => {
const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
entityFile.addStatements([
'// This file was autogenerated. Please do not change.',
'// All changes will be overwrited on commit.',
'',
]);
const { properties: sProps, required } = this.schemas[sName];
const importEntities: { type: string, isClass: boolean }[] = [];
const entityInterface = entityFile.addInterface({
name: `I${sName}`,
isExported: true,
});
const sortedSProps = Object.keys(sProps || {}).sort();
// add server response interface to entityFile
sortedSProps.forEach((sPropName) => {
const [
pType, isArray, isClass, isImport, isAdditional
] = schemaParamParser(sProps[sPropName], this.openapi);
if (isImport) {
importEntities.push({ type: pType, isClass });
}
const propertyType = isAdditional
? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
entityInterface.addProperty({
name: sPropName,
type: propertyType,
hasQuestionToken: !(
(required && required.includes(sPropName)) || sProps[sPropName].required
),
});
});
// add import
const imports: { type: string, isClass: boolean }[] = [];
const types: string[] = [];
importEntities.forEach((i) => {
const { type } = i;
if (!types.includes(type)) {
imports.push(i);
types.push(type);
}
});
imports.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
const { type: pType, isClass } = ie;
if (isClass) {
entityFile.addImportDeclaration({
defaultImport: pType,
moduleSpecifier: `./${pType}`,
namedImports: [`I${pType}`],
});
} else {
entityFile.addImportDeclaration({
moduleSpecifier: `./${pType}`,
namedImports: [pType],
});
}
});
const entityClass = entityFile.addClass({
name: sName,
isDefaultExport: true,
});
// addProperties to class;
sortedSProps.forEach((sPropName) => {
const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
const isRequred = (required && required.includes(sPropName))
|| sProps[sPropName].required;
const propertyType = isAdditional
? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;
entityClass.addProperty({
name: `_${sPropName}`,
isReadonly: true,
type: propertyType,
});
const getter = entityClass.addGetAccessor({
name: toCamel(sPropName),
returnType: propertyType,
statements: [`return this._${sPropName};`],
});
const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName];
if (description || example) {
getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
}
if (minItems) {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MinItems`,
statements: [`return ${minItems};`],
});
}
if (maxItems) {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MaxItems`,
statements: [`return ${maxItems};`],
});
}
if (typeof minLength === 'number') {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MinLength`,
statements: [`return ${minLength};`],
});
}
if (maxLength) {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MaxLength`,
statements: [`return ${maxLength};`],
});
}
if (typeof minimum === 'number') {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MinValue`,
statements: [`return ${minimum};`],
});
}
if (maximum) {
entityClass.addGetAccessor({
isStatic: true,
name: `${toCamel(sPropName)}MaxValue`,
statements: [`return ${maximum};`],
});
}
if (!(isArray && isClass) && !isClass) {
const isEnum = !isClass && isImport;
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
if (isRequired || haveValidationFields) {
const prop = toCamel(sPropName);
const validateField = entityClass.addMethod({
isStatic: true,
name: `${prop}Validate`,
returnType: `boolean`,
parameters: [{
name: prop,
type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
}],
})
validateField.setBodyText((w) => {
w.write('return ');
const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
if (pType === 'string') {
if (isArray) {
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
} else {
if (typeof minLength === 'number' && maxLength) {
w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
}
if (typeof minLength !== 'number' || !maxLength) {
w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
}
}
} else if (pType === 'number') {
if (isArray) {
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
} else {
if (typeof minimum === 'number' && maximum) {
w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
}
if (typeof minimum !== 'number' || !maximum) {
w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
}
}
} else if (pType === 'boolean') {
w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
} else if (isEnum) {
if (isArray){
w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
} else {
w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
}
}
w.write(';');
});
}
}
});
// add constructor;
const ctor = entityClass.addConstructor({
parameters: [{
name: 'props',
type: `I${sName}`,
}],
});
ctor.setBodyText((w) => {
sortedSProps.forEach((sPropName) => {
const [
pType, isArray, isClass, , isAdditional
] = schemaParamParser(sProps[sPropName], this.openapi);
const req = (required && required.includes(sPropName))
|| sProps[sPropName].required;
if (!req) {
if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
} else {
w.writeLine(`if (props.${sPropName}) {`);
}
}
if (isAdditional) {
if (isArray && isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
return { ...prev, [key]: new ${pType}(p[key])};
},{}))`);
} else if (isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
},{})`);
} else {
if (pType === 'string' && !isArray) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: props.${sPropName}[key].trim()};
},{})`);
} else {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
return { ...prev, [key]: props.${sPropName}[key]};
},{})`);
}
}
} else {
if (isArray && isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
} else if (isClass) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
} else {
if (pType === 'string' && !isArray) {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
} else {
w.writeLine(`${!req ? ' ' : ''}this._${sPropName} = props.${sPropName};`);
}
}
}
if (!req) {
w.writeLine('}');
}
});
});
// add serialize method;
const serialize = entityClass.addMethod({
isStatic: false,
name: 'serialize',
returnType: `I${sName}`,
});
serialize.setBodyText((w) => {
w.writeLine(`const data: I${sName} = {`);
const unReqFields: string[] = [];
sortedSProps.forEach((sPropName) => {
const req = (required && required.includes(sPropName))
|| sProps[sPropName].required;
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
if (!req) {
unReqFields.push(sPropName);
return;
}
if (isAdditional) {
if (isArray && isClass) {
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
} else if (isClass) {
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
} else {
w.writeLine(` ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
}
} else {
if (isArray && isClass) {
w.writeLine(` ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
} else if (isClass) {
w.writeLine(` ${sPropName}: this._${sPropName}.serialize(),`);
} else {
w.writeLine(` ${sPropName}: this._${sPropName},`);
}
}
});
w.writeLine('};');
unReqFields.forEach((sPropName) => {
const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
if (isAdditional) {
if (isArray && isClass) {
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
} else if (isClass) {
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
} else {
w.writeLine(` data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
}
} else {
if (isArray && isClass) {
w.writeLine(` data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
} else if (isClass) {
w.writeLine(` data.${sPropName} = this._${sPropName}.serialize();`);
} else {
w.writeLine(` data.${sPropName} = this._${sPropName};`);
}
}
w.writeLine(`}`);
});
w.writeLine('return data;');
});
// add validate method
const validate = entityClass.addMethod({
isStatic: false,
name: 'validate',
returnType: `string[]`,
})
validate.setBodyText((w) => {
w.writeLine('const validate = {');
Object.keys(sProps || {}).forEach((sPropName) => {
const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;
if (isArray && isClass) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
} else if (isClass && !isAdditional) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);
} else {
if (pType === 'string') {
if (isArray) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`);
} else {
if (typeof minLength === 'number' && maxLength) {
w.writeLine(` ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
}
if (typeof minLength !== 'number' || !maxLength) {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`);
}
}
} else if (pType === 'number') {
if (isArray) {
w.writeLine(` ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`);
} else {
if (typeof minimum === 'number' && maximum) {
w.writeLine(` ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`);
}
if (typeof minimum !== 'number' || !maximum) {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`);
}
}
} else if (pType === 'boolean') {
w.writeLine(` ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
}
}
});
w.writeLine('};');
w.writeLine('const isError: string[] = [];')
w.writeLine('Object.keys(validate).forEach((key) => {');
w.writeLine(' if (!(validate as any)[key]) {');
w.writeLine(' isError.push(key);');
w.writeLine(' }');
w.writeLine('});');
w.writeLine('return isError;');
});
// add update method;
const update = entityClass.addMethod({
isStatic: false,
name: 'update',
returnType: `${sName}`,
});
update.addParameter({
name: 'props',
type: `Partial<I${sName}>`,
});
update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });
this.entities.push(entityFile);
};
save = () => {
this.entities.forEach(async (e) => {
await e.saveSync();
});
};
}
export default EntitiesGenerator;

View File

@@ -0,0 +1,74 @@
const toCamel = (s: string) => {
return s.replace(/([-_][a-z])/ig, ($1) => {
return $1.toUpperCase()
.replace('-', '')
.replace('_', '');
});
};
const capitalize = (s: string) => {
return s[0].toUpperCase() + s.slice(1);
};
const TYPES = {
integer: 'number',
float: 'number',
number: 'number',
string: 'string',
boolean: 'boolean',
};
/**
* @param schemaProp: valueof shema.properties[key]
* @param openApi: openapi object
* @returns [propType - basicType or import one, isArray, isClass, isImport]
*/
const schemaParamParser = (schemaProp: any, openApi: any): [string, boolean, boolean, boolean, boolean] => {
let type = '';
let isImport = false;
let isClass = false;
let isArray = false;
let isAdditional = false;
if (schemaProp.$ref || schemaProp.additionalProperties?.$ref) {
const temp = (schemaProp.$ref || schemaProp.additionalProperties?.$ref).split('/');
if (schemaProp.additionalProperties) {
isAdditional = true;
}
type = `${temp[temp.length - 1]}`;
const cl = openApi ? openApi.components.schemas[temp[temp.length - 1]] : {};
if (cl.type === 'string' && cl.enum) {
isImport = true;
}
if (cl.type === 'object' && !cl.oneOf) {
isClass = true;
isImport = true;
} else if (cl.type === 'array') {
const temp: any = schemaParamParser(cl.items, openApi);
type = `${temp[0]}`;
isArray = true;
isClass = isClass || temp[2];
isImport = isImport || temp[3];
}
} else if (schemaProp.type === 'array') {
const temp: any = schemaParamParser(schemaProp.items, openApi);
type = `${temp[0]}`;
isArray = true;
isClass = isClass || temp[2];
isImport = isImport || temp[3];
} else {
type = (TYPES as Record<any, string>)[schemaProp.type];
}
if (!type) {
// TODO: Fix bug with Error fields.
type = 'any';
// throw new Error('Failed to find entity type');
}
return [type, isArray, isClass, isImport, isAdditional];
};
export { TYPES, toCamel, capitalize, schemaParamParser };

View File

@@ -0,0 +1,226 @@
import * as fs from 'fs';
import {
Project,
VariableStatement,
SyntaxKind,
Node,
Statement,
ts,
Identifier,
SourceFile,
} from 'ts-morph';
import {
LOCALE_FOLDER_PATH,
TRANSLATOR_CLASS_NAME,
USE_INTL_NAME,
trimQuotes,
} from '../consts';
import { checkForms, AvailableLocales } from '../../src/localization/Translator';
const project = new Project({
tsConfigFilePath: './tsconfig.json',
});
let lang = 'ru';
let option = '';
if (process.argv.length > 2) {
lang = process.argv[2];
option = process.argv[3];
}
const usedTranslations: string[] = [];
const usedPluralTranslations: string[] = [];
const problemFiles: string[] = [];
const sourceFiles = project.getSourceFiles();
const sourceFilesWithIntl = sourceFiles.filter((sf) => {
return !!sf.getImportDeclarations().find((id) => {
return !!id.getNamedImports().find((ni) => ni.getName() === USE_INTL_NAME)
})
});
const getFileUsedIntl = (statements: Statement<ts.Statement>[]) => {
statements.forEach((s) => {
if (s instanceof VariableStatement) {
s.forEachDescendant((node) => {
let intVariableDeclaration: Identifier = null;
switch (node.getKind()) {
case SyntaxKind.VariableDeclaration:
if (node.getSymbol()) {
const name = node.getSymbol().getName();
const callExp = node.getChildren().find((n) => n.getKind() === SyntaxKind.CallExpression);
if (callExp) {
const callExpIden = callExp.getChildren().find(n => n.getKind() === SyntaxKind.Identifier);
if (callExpIden && callExpIden.getSymbol().getName() === USE_INTL_NAME) {
intVariableDeclaration = node as Identifier;
}
}
}
break;
default:
break;
}
if (intVariableDeclaration) {
intVariableDeclaration.findReferencesAsNodes().forEach((fr) => {
if (fr instanceof Node) {
const parent = fr.getParentIfKind(SyntaxKind.PropertyAccessExpression);
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
if (syntaxList) {
const id = syntaxList.getChildren()[0];
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
problemFiles.push(fr.getSourceFile().getFilePath());
}
if (id) {
usedTranslations.push(trimQuotes(id.getText()));
if (parent.getName() === 'getPlural') {
usedPluralTranslations.push(trimQuotes(id.getText()));
}
}
}
}
}
})
}
});
}
})
}
const getFileUsedTranslations = (file: SourceFile) => {
const namedImport = file.getImportDeclarations().find((id) => !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME));
if (namedImport) {
const identifier = namedImport.getImportClause().getNamedImports().find((iden) => iden.getName() === TRANSLATOR_CLASS_NAME);
const translateReferences = identifier.getNodeProperty('name').findReferencesAsNodes();
if (translateReferences.length > 0) {
translateReferences.forEach((identifierNode) => {
if (identifierNode.getParentIfKind(SyntaxKind.TypeReference)) {
const translatorVariable = identifierNode.getParent().getPreviousSibling().getPreviousSiblingIfKind(SyntaxKind.Identifier);
if (translatorVariable) {
translatorVariable.findReferencesAsNodes().forEach((node) => {
const parent = node.getParentIfKind(SyntaxKind.PropertyAccessExpression);
if (parent && (parent.getName() === 'getMessage' || parent.getName() === 'getPlural')) {
const syntaxList = parent.getNextSiblings().find((n) => n.getKind() === SyntaxKind.SyntaxList);
if (syntaxList) {
const id = syntaxList.getChildren()[0];
if (id && id.getKind() !== SyntaxKind.StringLiteral) {
problemFiles.push(parent.getSourceFile().getFilePath());
}
if (id) {
usedTranslations.push(trimQuotes(id.getText()));
if (parent.getName() === 'getPlural') {
usedPluralTranslations.push(trimQuotes(id.getText()));
}
}
}
}
})
}
}
})
}
}
}
sourceFilesWithIntl.forEach((file) => {
getFileUsedIntl(file.getStatements());
})
const sourceFilesWithTranslator = project.getSourceFiles().filter((sf) => {
return !!sf.getImportDeclarations().find((id) => {
return !!id.getNamedImports().find((ni) => ni.getName() === TRANSLATOR_CLASS_NAME)
})
});
sourceFilesWithTranslator.forEach((file) => {
getFileUsedTranslations(file);
})
const filteredUsedTranslations = Array.from(new Set(usedTranslations));
const filteredUsedPluralTranslations = Array.from(new Set(usedPluralTranslations));
if (problemFiles.length) {
console.warn(`\n============== Files where translation id provided not as string ==============\n`);
console.log(problemFiles.join('\n'));
process.exit(255);
}
const allFiles = fs.readdirSync(LOCALE_FOLDER_PATH);
// Use ru or needed language
const translationFile = allFiles.find((file) => file.includes(`${lang}.json`));
if (!translationFile) {
console.error('File not found');
process.exit(255);
}
const translationsObject = JSON.parse(fs.readFileSync(`./src/lib/intl/__locales/${translationFile}`, { flag: 'r+' }) as unknown as string);
const translations = {
locale: translationFile,
messages: Object.keys(translationsObject),
};
const someMessagesNotFound: string[] = [];
const notUsed: string[] = [];
const notFound: string[] = [];
const checkLocaleMessages = (locale: string, messages: string[]) => {
filteredUsedTranslations.forEach(f => {
if (!messages.includes(f)) {
notFound.push(f);
}
});
messages.forEach(t => {
if (!filteredUsedTranslations.includes(t)) {
notUsed.push(t);
}
});
if (notFound.length > 0) {
someMessagesNotFound.push(locale);
}
}
const render = (data: string[], title: string) => {
console.log(`============ ${title} ============`);
console.table(data);
console.log(`============ ${title} ============`);
}
checkLocaleMessages(translations.locale, translations.messages);
const checkPluralForm = () => {
const pluralFormWrong: string[] = [];
filteredUsedPluralTranslations.forEach((id) => {
const message = translationsObject[id];
if (!checkForms(message, lang as AvailableLocales, id)) {
pluralFormWrong.push(id)
}
});
return pluralFormWrong;
}
const plural = checkPluralForm();
if (!option && (someMessagesNotFound.length || plural.length > 0 )) {
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n`));
plural.forEach(id => console.error(`\nTranslation with id: "${id}" - have wrong number of plural forms!\n`));
process.exit(255);
}
if (option) {
switch (option) {
case '--show-missing': {
render(notFound, 'NotFound')
break;
}
case '--show-unused': {
render(notUsed, 'notUsed')
break;
}
case '--check-plurals': {
render(plural, 'Wrong Plural Form')
}
default: {
if (someMessagesNotFound.length) {
someMessagesNotFound.forEach(locale => console.error(`\nSome translatins for ${locale} was not found!\n\n`));
process.exit(255);
}
}
}
}

View File

@@ -0,0 +1,79 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
ecmaFeatures: {
jsx: true
},
extraFileExtensions: ['mjs', 'tsx', 'ts'],
ecmaVersion: 2020,
sourceType: 'module'
},
plugins: ['react', '@typescript-eslint', 'import'],
env: {
browser: true,
commonjs: true,
es6: true,
es2020: true,
jest: true,
},
settings: {
react: {
pragma: 'React',
version: 'detect',
},
'import/resolver': {
typescript: {
alwaysTryTypes: true
}
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/explicit-function-return-type': [0, { allowExpressions: true }],
'@typescript-eslint/indent': ['error', 4],
'@typescript-eslint/interface-name-prefix': [0, { prefixWithI: 'never' }],
'@typescript-eslint/no-explicit-any': [0],
'@typescript-eslint/naming-convention': [2, {
selector: 'enum', format: ['UPPER_CASE', 'PascalCase'],
}],
'@typescript-eslint/no-non-null-assertion': 0,
'arrow-body-style': 'off',
'consistent-return': 0,
curly: [2, 'all'],
'default-case': 0,
'import/no-cycle': 0,
'import/prefer-default-export': 'off',
'import/no-named-as-default': 0,
indent: [0, 4],
'no-alert': 2,
'no-console': 2,
'no-debugger': 2,
'no-underscore-dangle': 'off',
'no-useless-escape': 'off',
'object-curly-newline': 'off',
'react-hooks/exhaustive-deps': 0,
'react/display-name': 0,
'react/jsx-indent-props': ['error', 4],
'react/jsx-indent': ['error', 4],
'react/jsx-one-expression-per-line': 'off',
'react/jsx-props-no-spreading': 0,
'react/prop-types': 'off',
'react/state-in-constructor': 'off',
},
extends: [
'airbnb-base',
'airbnb-typescript/base',
'airbnb/hooks',
'plugin:react/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
globals: {},
};

View File

@@ -0,0 +1,10 @@
module.exports = {
rules: {
'no-alert': 0,
'no-debugger': 0,
'no-console': 0,
},
extends: [
'./common',
],
};

View File

@@ -0,0 +1,5 @@
module.exports = {
extends: [
'./common.js',
],
};

View File

@@ -0,0 +1,40 @@
const yaml = require('yaml');
const fs = require('fs');
const ZERO_HOST = '0.0.0.0';
const LOCALHOST = '127.0.0.1';
const DEFAULT_PORT = 80;
const importConfig = () => {
try {
const doc = yaml.parse(fs.readFileSync('../AdguardHome.yaml', 'utf8'));
const { bind_host, bind_port } = doc;
return {
bind_host,
bind_port,
};
} catch (e) {
return {
bind_host: ZERO_HOST,
bind_port: DEFAULT_PORT,
};
}
};
const getDevServerConfig = () => {
const { bind_host: host, bind_port: port } = importConfig();
const { DEV_SERVER_PORT } = process.env;
const devServerHost = host === ZERO_HOST ? LOCALHOST : host;
const devServerPort = 3000 || port + 8000;
return {
host: devServerHost,
port: devServerPort
};
};
module.exports = {
importConfig,
getDevServerConfig
};

View File

@@ -0,0 +1,74 @@
const path = require('path');
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const tsconfig = require('../../tsconfig.json');
const RESOURCES_PATH = path.resolve(__dirname, '../../');
const HTML_PATH = path.resolve(RESOURCES_PATH, 'public/index.html');
const HTML_INSTALL_PATH = path.resolve(RESOURCES_PATH, 'public/install.html');
module.exports = {
entry: {
install: './src/Install.tsx',
main: './src/App.tsx'
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.pcss'],
alias: Object.keys(tsconfig.compilerOptions.paths).reduce((aliases, key) => {
// Reduce to load aliases from ./tsconfig.json in appropriate for webpack form
const paths = tsconfig.compilerOptions.paths[key].map(p => p.replace('/*', ''));
aliases[key.replace('/*', '')] = path.resolve(
__dirname,
'../../',
tsconfig.compilerOptions.baseUrl,
...paths,
);
return aliases;
}, {}),
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.(woff|woff2)$/,
use: [{
loader: 'file-loader',
options:{
outputPath:'./',
}
}],
},
{
test:/\.(png|jpe?g|gif)$/,
exclude: /(node_modules)/,
use:[{
loader:'file-loader',
options:{
outputPath:'./images',
}
}]
}
],
},
plugins: [
// new AntdDayjsWebpackPlugin()
new HtmlWebpackPlugin({
inject: true,
cache: false,
chunks: ['main'],
template: HTML_PATH,
}),
new HtmlWebpackPlugin({
inject: true,
cache: false,
chunks: ['install'],
filename: 'install.html',
template: HTML_INSTALL_PATH,
}),
],
};

View File

@@ -0,0 +1,113 @@
const history = require('connect-history-api-fallback');
const { merge } = require('webpack-merge');
const path = require('path');
const proxy = require('http-proxy-middleware');
const Webpack = require('webpack');
const { getDevServerConfig } = require('./helpers');
const baseConfig = require('./webpack.config.base');
const target = getDevServerConfig();
const options = {
target: `http://${target.host}:${target.port}`, // target host
changeOrigin: true, // needed for virtual hosted sites
};
const apiProxy = proxy.createProxyMiddleware(options);
module.exports = merge(baseConfig, {
mode: 'development',
output: {
path: path.resolve(__dirname, '../../build2'),
filename: '[name].bundle.js',
},
optimization: {
noEmitOnErrors: true,
},
devServer: {
port: 4000,
historyApiFallback: true,
before: (app) => {
app.use('/control', apiProxy);
app.use(history({
rewrites: [
{
from: /\.(png|jpe?g|gif)$/,
to: (context) => {
const name = context.parsedUrl.pathname.split('/');
return `/images/${name[name.length - 1]}`
}
}, {
from: /\.(woff|woff2)$/,
to: (context) => {
const name = context.parsedUrl.pathname.split('/');
return `/${name[name.length - 1]}`
}
}, {
from: /\.(js|css)$/,
to: (context) => {
const name = context.parsedUrl.pathname.split('/');
return `/${name[name.length - 1]}`
}
}
],
}));
}
},
devtool: 'eval-source-map',
module: {
rules: [
{
enforce: 'pre',
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
configFile: path.resolve(__dirname, '../lint/dev.js'),
}
},
{
test: (resource) => {
return (
resource.indexOf('.pcss')+1
|| resource.indexOf('.css')+1
|| resource.indexOf('.less')+1
) && !(resource.indexOf('.module.')+1);
},
use: ['style-loader', 'css-loader', 'postcss-loader', {
loader: 'less-loader',
options: {
javascriptEnabled: true,
},
}],
},
{
test: /\.module\.p?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
modules: {
localIdentName: "[name]__[local]___[hash:base64:5]",
}
},
},
'postcss-loader',
],
exclude: /node_modules/,
},
]
},
plugins: [
new Webpack.DefinePlugin({
DEV: true,
'process.env.DEV_SERVER_PORT': JSON.stringify(3000),
}),
new Webpack.HotModuleReplacementPlugin(),
new Webpack.ProgressPlugin(),
],
});

View File

@@ -0,0 +1,91 @@
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = require('./webpack.config.base');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const Webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(baseConfig, {
mode: 'production',
devtool: 'source-map',
output: {
path: path.resolve(__dirname, '../../../build2/static'),
filename: '[name].bundle.[hash:5].js',
publicPath: '/'
},
optimization: {
minimizer: [new TerserJSPlugin({terserOptions: {
output: {
comments: false,
},
},
extractComments: false,
}), new OptimizeCSSAssetsPlugin({})],
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
},
module: {
rules: [
{
test: (resource) => {
return (
resource.indexOf('.pcss')+1
|| resource.indexOf('.css')+1
|| resource.indexOf('.less')+1
) && !(resource.indexOf('.module.')+1);
},
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
esModules: true,
}
}, 'css-loader', 'postcss-loader', {
loader: 'less-loader',
options: {
javascriptEnabled: true,
},
}],
exclude: /node_modules/,
},
{
test: /\.module\.p?css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
esModules: true,
}
},
{
loader: 'css-loader',
options: {
modules: true,
sourceMap: true,
importLoaders: 1,
},
},
'postcss-loader',
],
exclude: /node_modules/,
}
]
},
plugins: [
new Webpack.DefinePlugin({
DEV: false,
}),
new MiniCssExtractPlugin({
filename: '[name].[hash:5].css',
}),
]
});