mirror of
https://github.com/danielsogl/awesome-cordova-plugins.git
synced 2025-03-04 00:13:06 +08:00
refactor(build): lint build scripts
This commit is contained in:
parent
d7829e4012
commit
c15b78bab2
@ -5,12 +5,13 @@ import { camelCase, clone } from 'lodash';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
export const ROOT = path.resolve(__dirname, '../../');
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
export const TS_CONFIG = clone(require(path.resolve(ROOT, 'tsconfig.json')));
|
||||
export const COMPILER_OPTIONS = TS_CONFIG.compilerOptions;
|
||||
export const PLUGINS_ROOT = path.join(ROOT, 'src/@ionic-native/plugins/');
|
||||
export const PLUGIN_PATHS = fs.readdirSync(PLUGINS_ROOT).map(d => path.join(PLUGINS_ROOT, d, 'index.ts'));
|
||||
|
||||
export function getDecorator(node: ts.Node, index: number = 0): ts.Decorator {
|
||||
export function getDecorator(node: ts.Node, index = 0): ts.Decorator {
|
||||
if (node.decorators && node.decorators[index])
|
||||
return node.decorators[index];
|
||||
}
|
||||
@ -59,7 +60,7 @@ export function getDecoratorArgs(decorator: any) {
|
||||
|
||||
default:
|
||||
Logger.debug('Unexpected property value type: ' + prop.initializer.kind);
|
||||
throw 'Unexpected property value type << helpers.ts >>';
|
||||
throw new Error('Unexpected property value type << helpers.ts >>');
|
||||
}
|
||||
|
||||
args[prop.name.text] = val;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import { hasDecorator, ROOT } from '../helpers';
|
||||
|
||||
export interface InjectableClassEntry {
|
||||
@ -24,24 +25,28 @@ export function extractInjectables() {
|
||||
return (ctx: ts.TransformationContext) => {
|
||||
return tsSourceFile => {
|
||||
if (tsSourceFile.fileName.indexOf('src/@ionic-native/plugins') > -1) {
|
||||
ts.visitEachChild(tsSourceFile, node => {
|
||||
if (node.kind !== ts.SyntaxKind.ClassDeclaration) {
|
||||
return node;
|
||||
}
|
||||
ts.visitEachChild(
|
||||
tsSourceFile,
|
||||
node => {
|
||||
if (node.kind !== ts.SyntaxKind.ClassDeclaration) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const isInjectable: boolean = hasDecorator('Injectable', node);
|
||||
if (isInjectable) {
|
||||
injectableClasses.push({
|
||||
file: tsSourceFile.path,
|
||||
className: (node as ts.ClassDeclaration).name.text,
|
||||
dirName: tsSourceFile.path.split(/[\\\/]+/).reverse()[1]
|
||||
});
|
||||
}
|
||||
}, ctx);
|
||||
const isInjectable: boolean = hasDecorator('Injectable', node);
|
||||
if (isInjectable) {
|
||||
injectableClasses.push({
|
||||
file: tsSourceFile.path,
|
||||
className: (node as ts.ClassDeclaration).name.text,
|
||||
dirName: tsSourceFile.path.split(/[\\\/]+/).reverse()[1]
|
||||
});
|
||||
}
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
return tsSourceFile;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ function transformImports(file: ts.SourceFile, ctx: ts.TransformationContext, ng
|
||||
// we're only interested in files containing @ionic-native/core import statement
|
||||
if (!importStatement) return file;
|
||||
|
||||
let decorators: string[] = [];
|
||||
const decorators: string[] = [];
|
||||
|
||||
const decoratorRegex: RegExp = /@([a-zA-Z]+)\(/g;
|
||||
|
||||
@ -48,6 +48,6 @@ export function importsTransformer(ngcBuild?: boolean) {
|
||||
return (ctx: ts.TransformationContext) => {
|
||||
return tsSourceFile => {
|
||||
return transformImports(tsSourceFile, ctx, ngcBuild);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { transformProperty } from './properties';
|
||||
export function transformMembers(cls: ts.ClassDeclaration) {
|
||||
const propertyIndices: number[] = [];
|
||||
|
||||
let members = cls.members.map((member: any, index: number) => {
|
||||
const members = cls.members.map((member: any, index: number) => {
|
||||
// only process decorated members
|
||||
if (!member.decorators || !member.decorators.length) return member;
|
||||
|
||||
|
@ -1,58 +1,74 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import { Logger } from '../../logger';
|
||||
import { convertValueToLiteral, getDecorator, getDecoratorArgs, getDecoratorName } from '../helpers';
|
||||
import {
|
||||
convertValueToLiteral,
|
||||
getDecorator,
|
||||
getDecoratorArgs,
|
||||
getDecoratorName
|
||||
} from '../helpers';
|
||||
import { transformMembers } from './members';
|
||||
|
||||
function transformClass(cls: any, ngcBuild?: boolean) {
|
||||
|
||||
Logger.profile('transformClass: ' + cls.name.text);
|
||||
|
||||
const pluginStatics = [];
|
||||
const dec: any = getDecorator(cls);
|
||||
const pluginStatics = [];
|
||||
const dec: any = getDecorator(cls);
|
||||
|
||||
if (dec) {
|
||||
const pluginDecoratorArgs = getDecoratorArgs(dec);
|
||||
if (dec) {
|
||||
const pluginDecoratorArgs = getDecoratorArgs(dec);
|
||||
|
||||
// add plugin decorator args as static properties of the plugin's class
|
||||
for (let prop in pluginDecoratorArgs) {
|
||||
pluginStatics.push(ts.createProperty(
|
||||
// add plugin decorator args as static properties of the plugin's class
|
||||
for (const prop in pluginDecoratorArgs) {
|
||||
pluginStatics.push(
|
||||
ts.createProperty(
|
||||
undefined,
|
||||
[ts.createToken(ts.SyntaxKind.StaticKeyword)],
|
||||
ts.createIdentifier(prop),
|
||||
undefined,
|
||||
undefined,
|
||||
convertValueToLiteral(pluginDecoratorArgs[prop])
|
||||
));
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
cls = ts.createClassDeclaration(
|
||||
ngcBuild && cls.decorators && cls.decorators.length? cls.decorators.filter(d => getDecoratorName(d) === 'Injectable') : undefined, // remove Plugin and Injectable decorators
|
||||
[ts.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
cls.name,
|
||||
cls.typeParameters,
|
||||
cls.heritageClauses,
|
||||
[
|
||||
...transformMembers(cls),
|
||||
...pluginStatics
|
||||
]
|
||||
);
|
||||
cls = ts.createClassDeclaration(
|
||||
ngcBuild && cls.decorators && cls.decorators.length
|
||||
? cls.decorators.filter(d => getDecoratorName(d) === 'Injectable')
|
||||
: undefined, // remove Plugin and Injectable decorators
|
||||
[ts.createToken(ts.SyntaxKind.ExportKeyword)],
|
||||
cls.name,
|
||||
cls.typeParameters,
|
||||
cls.heritageClauses,
|
||||
[...transformMembers(cls), ...pluginStatics]
|
||||
);
|
||||
|
||||
Logger.profile('transformClass: ' + cls.name.text, { level: 'verbose' });
|
||||
return cls;
|
||||
Logger.profile('transformClass: ' + cls.name.text, { level: 'verbose' });
|
||||
return cls;
|
||||
}
|
||||
|
||||
function transformClasses(file: ts.SourceFile, ctx: ts.TransformationContext, ngcBuild?: boolean) {
|
||||
function transformClasses(
|
||||
file: ts.SourceFile,
|
||||
ctx: ts.TransformationContext,
|
||||
ngcBuild?: boolean
|
||||
) {
|
||||
Logger.silly('Transforming file: ' + file.fileName);
|
||||
return ts.visitEachChild(file, node => {
|
||||
if (node.kind !== ts.SyntaxKind.ClassDeclaration) {
|
||||
return node;
|
||||
}
|
||||
return transformClass(node, ngcBuild);
|
||||
}, ctx);
|
||||
return ts.visitEachChild(
|
||||
file,
|
||||
node => {
|
||||
if (node.kind !== ts.SyntaxKind.ClassDeclaration) {
|
||||
return node;
|
||||
}
|
||||
return transformClass(node, ngcBuild);
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
export function pluginClassTransformer(ngcBuild?: boolean): ts.TransformerFactory<ts.SourceFile> {
|
||||
export function pluginClassTransformer(
|
||||
ngcBuild?: boolean
|
||||
): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (ctx: ts.TransformationContext) => {
|
||||
return tsSourceFile => {
|
||||
if (tsSourceFile.fileName.indexOf('src/@ionic-native/plugins') > -1)
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import { getDecorator, getDecoratorName } from '../helpers';
|
||||
|
||||
export function transformProperty(members: any[], index: number) {
|
||||
|
||||
const property = members[index] as ts.PropertyDeclaration,
|
||||
decorator = getDecorator(property),
|
||||
decoratorName = getDecoratorName(decorator);
|
||||
@ -18,36 +18,50 @@ export function transformProperty(members: any[], index: number) {
|
||||
type = 'instance';
|
||||
break;
|
||||
|
||||
default: return property;
|
||||
default:
|
||||
return property;
|
||||
}
|
||||
|
||||
|
||||
const getter = ts.createGetAccessor(undefined, undefined, property.name, undefined, property.type, ts.createBlock([
|
||||
ts.createReturn(
|
||||
ts.createCall(
|
||||
ts.createIdentifier(type + 'PropertyGet'),
|
||||
undefined,
|
||||
[
|
||||
const getter = ts.createGetAccessor(
|
||||
undefined,
|
||||
undefined,
|
||||
property.name,
|
||||
undefined,
|
||||
property.type,
|
||||
ts.createBlock([
|
||||
ts.createReturn(
|
||||
ts.createCall(ts.createIdentifier(type + 'PropertyGet'), undefined, [
|
||||
ts.createThis(),
|
||||
ts.createLiteral((property.name as any).text)
|
||||
]
|
||||
])
|
||||
)
|
||||
)
|
||||
]));
|
||||
])
|
||||
);
|
||||
|
||||
const setter = ts.createSetAccessor(undefined, undefined, property.name, [ts.createParameter(undefined, undefined, undefined, 'value', undefined, property.type)], ts.createBlock([
|
||||
ts.createStatement(
|
||||
ts.createCall(
|
||||
ts.createIdentifier(type + 'PropertySet'),
|
||||
const setter = ts.createSetAccessor(
|
||||
undefined,
|
||||
undefined,
|
||||
property.name,
|
||||
[
|
||||
ts.createParameter(
|
||||
undefined,
|
||||
[
|
||||
undefined,
|
||||
undefined,
|
||||
'value',
|
||||
undefined,
|
||||
property.type
|
||||
)
|
||||
],
|
||||
ts.createBlock([
|
||||
ts.createStatement(
|
||||
ts.createCall(ts.createIdentifier(type + 'PropertySet'), undefined, [
|
||||
ts.createThis(),
|
||||
ts.createLiteral((property.name as any).text),
|
||||
ts.createIdentifier('value')
|
||||
]
|
||||
])
|
||||
)
|
||||
)
|
||||
]));
|
||||
])
|
||||
);
|
||||
|
||||
return [getter, setter];
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export function getCompilerHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
export function getProgram(declaration: boolean = false, pluginPaths: string[] = PLUGIN_PATHS) {
|
||||
export function getProgram(declaration = false, pluginPaths: string[] = PLUGIN_PATHS) {
|
||||
const compilerOptions: ts.CompilerOptions = clone(COMPILER_OPTIONS);
|
||||
compilerOptions.declaration = declaration;
|
||||
compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs;
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { createLogger, transports, format } from 'winston';
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
|
||||
const { label, printf, prettyPrint, combine, colorize, simple } = format;
|
||||
|
||||
const LOG_LEVEL = 'silly';
|
||||
|
||||
export const Logger = createLogger({
|
||||
level: LOG_LEVEL,
|
||||
format: combine(
|
||||
colorize(),
|
||||
simple(),
|
||||
),
|
||||
format: combine(colorize(), simple()),
|
||||
transports: [new transports.Console({ level: LOG_LEVEL })]
|
||||
});
|
||||
|
@ -1,18 +1,30 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as webpack from 'webpack';
|
||||
import * as uglifyJsPlugin from 'uglifyjs-webpack-plugin';
|
||||
import * as unminifiedPlugin from 'unminified-webpack-plugin';
|
||||
import { cleanEmittedData, EMIT_PATH, InjectableClassEntry } from '../build/transformers/extract-injectables';
|
||||
import * as webpack from 'webpack';
|
||||
|
||||
import { ROOT } from '../build/helpers';
|
||||
import {
|
||||
cleanEmittedData,
|
||||
EMIT_PATH,
|
||||
InjectableClassEntry
|
||||
} from '../build/transformers/extract-injectables';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
const DIST = path.resolve(ROOT, 'dist');
|
||||
const INDEX_PATH = path.resolve(DIST, 'index.js');
|
||||
const INJECTABLE_CLASSES = fs.readJSONSync(EMIT_PATH).map((item: InjectableClassEntry) => {
|
||||
item.file = './' + item.file.split(/[\/\\]+/).slice(-4, -1).join('/');
|
||||
return item;
|
||||
});
|
||||
const INJECTABLE_CLASSES = fs
|
||||
.readJSONSync(EMIT_PATH)
|
||||
.map((item: InjectableClassEntry) => {
|
||||
item.file =
|
||||
'./' +
|
||||
item.file
|
||||
.split(/[\/\\]+/)
|
||||
.slice(-4, -1)
|
||||
.join('/');
|
||||
return item;
|
||||
});
|
||||
|
||||
const webpackConfig: webpack.Configuration = {
|
||||
mode: 'production',
|
||||
@ -31,14 +43,16 @@ const webpackConfig: webpack.Configuration = {
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
use: path.resolve(ROOT, 'scripts/build/remove-tslib-helpers.js')
|
||||
}]
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: path.resolve(ROOT, 'scripts/build/remove-tslib-helpers.js')
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
'__extends': ['tslib', '__extends']
|
||||
__extends: ['tslib', '__extends']
|
||||
}),
|
||||
new webpack.optimize.OccurrenceOrderPlugin(true),
|
||||
new webpack.DefinePlugin({
|
||||
@ -52,7 +66,7 @@ const webpackConfig: webpack.Configuration = {
|
||||
};
|
||||
|
||||
function getPluginImport(entry: InjectableClassEntry) {
|
||||
return `import { ${ entry.className } } from '${ entry.file }';`;
|
||||
return `import { ${entry.className} } from '${entry.file}';`;
|
||||
}
|
||||
|
||||
function createIndexFile() {
|
||||
|
@ -1,27 +1,44 @@
|
||||
import { generateDeclarations, transpile } from '../build/transpile';
|
||||
import { EMIT_PATH } from '../build/transformers/extract-injectables';
|
||||
import { PLUGIN_PATHS } from '../build/helpers';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { PLUGIN_PATHS } from '../build/helpers';
|
||||
import { EMIT_PATH } from '../build/transformers/extract-injectables';
|
||||
import { generateDeclarations, transpile } from '../build/transpile';
|
||||
|
||||
generateDeclarations();
|
||||
transpile();
|
||||
|
||||
const outDirs = PLUGIN_PATHS.map(p => p.replace('src', 'dist').replace(/[\\/]index.ts/, ''));
|
||||
const outDirs = PLUGIN_PATHS.map(p =>
|
||||
p.replace('src', 'dist').replace(/[\\/]index.ts/, '')
|
||||
);
|
||||
const injectableClasses = fs.readJSONSync(EMIT_PATH);
|
||||
|
||||
|
||||
outDirs.forEach(dir => {
|
||||
const classes = injectableClasses.filter(entry => entry.dirName === dir.split(/[\\/]+/).pop());
|
||||
const classes = injectableClasses.filter(
|
||||
entry => entry.dirName === dir.split(/[\\/]+/).pop()
|
||||
);
|
||||
|
||||
let jsFile: string = fs.readFileSync(path.join(dir, 'index.js'), 'utf-8'),
|
||||
dtsFile: string = fs.readFileSync(path.join(dir, 'index.d.ts'), 'utf-8');
|
||||
|
||||
classes.forEach(entry => {
|
||||
dtsFile = dtsFile.replace(`class ${ entry.className } `, 'class ' + entry.className + 'Original ');
|
||||
dtsFile += `\nexport declare const ${ entry.className }: ${ entry.className }Original;`;
|
||||
jsFile = jsFile.replace(new RegExp(`([\\s\\(])${ entry.className }([\\s\\.;\\(,])`, 'g'), '$1' + entry.className + 'Original$2');
|
||||
jsFile = jsFile.replace(`export { ${ entry.className }Original }`, `var ${ entry.className } = new ${ entry.className }Original();\nexport { ${ entry.className } }`);
|
||||
dtsFile = dtsFile.replace(
|
||||
`class ${entry.className} `,
|
||||
'class ' + entry.className + 'Original '
|
||||
);
|
||||
dtsFile += `\nexport declare const ${entry.className}: ${
|
||||
entry.className
|
||||
}Original;`;
|
||||
jsFile = jsFile.replace(
|
||||
new RegExp(`([\\s\\(])${entry.className}([\\s\\.;\\(,])`, 'g'),
|
||||
'$1' + entry.className + 'Original$2'
|
||||
);
|
||||
jsFile = jsFile.replace(
|
||||
`export { ${entry.className}Original }`,
|
||||
`var ${entry.className} = new ${entry.className}Original();\nexport { ${
|
||||
entry.className
|
||||
} }`
|
||||
);
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(dir, 'index.js'), jsFile, 'utf-8');
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { generateDeclarationFiles, transpileNgx, transpileNgxCore, modifyMetadata, cleanupNgx } from '../build/ngx';
|
||||
import {
|
||||
cleanupNgx,
|
||||
generateDeclarationFiles,
|
||||
modifyMetadata,
|
||||
transpileNgx,
|
||||
transpileNgxCore
|
||||
} from '../build/ngx';
|
||||
|
||||
transpileNgxCore();
|
||||
transpileNgx();
|
||||
|
@ -1,25 +1,27 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { merge } from 'lodash';
|
||||
import { exec } from 'child_process';
|
||||
import { PLUGIN_PATHS, ROOT } from '../build/helpers';
|
||||
import { cpus } from 'os';
|
||||
import * as Queue from 'async-promise-queue';
|
||||
import { exec } from 'child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import { merge } from 'lodash';
|
||||
import { cpus } from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { PLUGIN_PATHS, ROOT } from '../build/helpers';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const MAIN_PACKAGE_JSON = require('../../package.json');
|
||||
const VERSION = MAIN_PACKAGE_JSON.version;
|
||||
const FLAGS = '--access public --tag beta';
|
||||
|
||||
const PACKAGE_JSON_BASE = {
|
||||
'description': 'Ionic Native - Native plugins for ionic apps',
|
||||
'module': 'index.js',
|
||||
'typings': 'index.d.ts',
|
||||
'author': 'ionic',
|
||||
'license': 'MIT',
|
||||
'repository': {
|
||||
'type': 'git',
|
||||
'url': 'https://github.com/ionic-team/ionic-native.git'
|
||||
description: 'Ionic Native - Native plugins for ionic apps',
|
||||
module: 'index.js',
|
||||
typings: 'index.d.ts',
|
||||
author: 'ionic',
|
||||
license: 'MIT',
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'https://github.com/ionic-team/ionic-native.git'
|
||||
}
|
||||
};
|
||||
|
||||
@ -32,7 +34,7 @@ const CORE_VERSION = '^5.0.0';
|
||||
|
||||
const PLUGIN_PEER_DEPENDENCIES = {
|
||||
'@ionic-native/core': VERSION, // TODO change this in production
|
||||
'rxjs': RXJS_VEERSION
|
||||
rxjs: RXJS_VEERSION
|
||||
};
|
||||
|
||||
function getPackageJsonContent(name, peerDependencies = {}) {
|
||||
@ -52,33 +54,40 @@ function writePackageJson(data: any, dir: string) {
|
||||
function prepare() {
|
||||
// write @ionic-native/core package.json
|
||||
writePackageJson(
|
||||
getPackageJsonContent('core', { 'rxjs': RXJS_VEERSION }),
|
||||
getPackageJsonContent('core', { rxjs: RXJS_VEERSION }),
|
||||
path.resolve(DIST, 'core')
|
||||
);
|
||||
|
||||
// write plugin package.json files
|
||||
PLUGIN_PATHS.forEach((pluginPath: string) => {
|
||||
const pluginName = pluginPath.split(/[\/\\]+/).slice(-2)[0];
|
||||
const packageJsonContents = getPackageJsonContent(pluginName, PLUGIN_PEER_DEPENDENCIES);
|
||||
const packageJsonContents = getPackageJsonContent(
|
||||
pluginName,
|
||||
PLUGIN_PEER_DEPENDENCIES
|
||||
);
|
||||
const dir = path.resolve(DIST, 'plugins', pluginName);
|
||||
|
||||
writePackageJson(packageJsonContents, dir);
|
||||
});
|
||||
}
|
||||
|
||||
async function publish(ignoreErrors: boolean = false) {
|
||||
async function publish(ignoreErrors = false) {
|
||||
Logger.profile('Publishing');
|
||||
// upload 1 package per CPU thread at a time
|
||||
const worker = Queue.async.asyncify((pkg: any) => {
|
||||
new Promise<any>((resolve, reject) => {
|
||||
exec(`npm publish ${ pkg } ${ FLAGS }`, (err, stdout) => {
|
||||
exec(`npm publish ${pkg} ${FLAGS}`, (err, stdout) => {
|
||||
if (stdout) {
|
||||
Logger.log(stdout.trim());
|
||||
resolve(stdout);
|
||||
}
|
||||
if (err) {
|
||||
if (!ignoreErrors) {
|
||||
if (err.message.includes('You cannot publish over the previously published version')) {
|
||||
if (
|
||||
err.message.includes(
|
||||
'You cannot publish over the previously published version'
|
||||
)
|
||||
) {
|
||||
Logger.verbose('Ignoring duplicate version error.');
|
||||
return resolve();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user