mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build: build material-examples with bazel (#13932)
This includes a `genrule` to create the generated `ExampleModule`
This commit is contained in:
committed by
Jeremy Elbourn
parent
150c964c32
commit
c64503e0bf
@@ -24,5 +24,10 @@
|
||||
"lib": ["es2015", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"types": ["tslib"]
|
||||
},
|
||||
"bazelOptions": {
|
||||
// Note: We can remove this once we fully switched away from Gulp. Currently we still set
|
||||
// some options here just in favor of the standard tsconfig's which extending this one.
|
||||
"suppressTsconfigOverrideWarnings": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package(default_visibility=["//visibility:public"])
|
||||
|
||||
load("@angular//:index.bzl", "ng_package")
|
||||
load("//:packages.bzl", "CDK_TARGETS", "MATERIAL_TARGETS", "ROLLUP_GLOBALS")
|
||||
load("//tools:defaults.bzl", "ng_module")
|
||||
|
||||
ng_module(
|
||||
name = "examples",
|
||||
srcs = glob(["**/*.ts"]) + [":example-module"],
|
||||
module_name = "@angular/material-examples",
|
||||
assets = glob(["**/*.html", "**/*.css"]),
|
||||
deps = [
|
||||
"@angular//packages/common",
|
||||
"@angular//packages/core",
|
||||
"@angular//packages/forms",
|
||||
"@npm//moment",
|
||||
"//src/material-moment-adapter",
|
||||
] + CDK_TARGETS + MATERIAL_TARGETS,
|
||||
# Specify the tsconfig that is also used by Gulp. We need to explicitly use this tsconfig
|
||||
# because in order to import Moment with TypeScript, some specific options need to be set.
|
||||
tsconfig = ":tsconfig-build.json",
|
||||
)
|
||||
|
||||
ng_package(
|
||||
name = "npm_package",
|
||||
srcs = ["package.json"],
|
||||
entry_point = "src/material-examples/public_api.js",
|
||||
globals = ROLLUP_GLOBALS,
|
||||
deps = [":examples"],
|
||||
# TODO(devversion): re-enable once we have set up the proper compiler for the ng_package
|
||||
tags = ["manual"],
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "example-module",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
outs = ["example-module.ts"],
|
||||
cmd = """
|
||||
# As a workaround for https://github.com/bazelbuild/rules_nodejs/issues/404, we pass the
|
||||
# data to the Bazel entry-point through environment variables.
|
||||
export _SOURCE_FILES="$(SRCS)"
|
||||
export _OUTPUT_FILE="$@"
|
||||
export _BASE_DIR="$$(dirname $(location //src/material-examples:index.ts))"
|
||||
|
||||
# Run the bazel entry-point for generating the example module.
|
||||
./$(location //tools/example-module:bazel-bin)
|
||||
""",
|
||||
tools = ["//tools/example-module:bazel-bin"],
|
||||
output_to_bindir = True,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// Unset options inherited from tsconfig-build
|
||||
"annotateForClosureCompiler": false,
|
||||
"flatModuleOutFile": null,
|
||||
"flatModuleId": null,
|
||||
"flatModuleId": null
|
||||
},
|
||||
"include": [
|
||||
"**/*.spec.ts",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package(default_visibility=["//visibility:public"])
|
||||
|
||||
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
ts_library(
|
||||
name = "example-module-lib",
|
||||
srcs = glob(["**/*.ts"]),
|
||||
deps = [
|
||||
"@npm//@types/node",
|
||||
"@npm//typescript",
|
||||
],
|
||||
tsconfig = ":tsconfig.json",
|
||||
)
|
||||
|
||||
nodejs_binary(
|
||||
name = "bazel-bin",
|
||||
entry_point = "angular_material/tools/example-module/bazel-bin.js",
|
||||
data = [
|
||||
"@npm//typescript",
|
||||
"@npm//source-map-support",
|
||||
":example-module-lib",
|
||||
":example-module.template",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import {generateExampleModule} from './generate-example-module';
|
||||
|
||||
/**
|
||||
* Entry point for the Bazel NodeJS target. Usually this would be a more generic CLI, but due to
|
||||
* Bazel not being able to handle a lot of files on Windows (with emulated Bash), we need to
|
||||
* read the arguments through environment variables which are handled better.
|
||||
*
|
||||
* - https://github.com/bazelbuild/rules_nodejs/issues/404
|
||||
* - https://github.com/bazelbuild/bazel/issues/3636
|
||||
*/
|
||||
|
||||
if (require.main === module) {
|
||||
const {_SOURCE_FILES, _OUTPUT_FILE, _BASE_DIR} = process.env;
|
||||
|
||||
generateExampleModule(_SOURCE_FILES.split(' '), _OUTPUT_FILE, _BASE_DIR);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
******************************************************************************
|
||||
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ExampleMaterialModule} from './material-module';
|
||||
|
||||
${exampleImports}
|
||||
|
||||
export interface LiveExample {
|
||||
title: string;
|
||||
component: any;
|
||||
additionalFiles?: string[];
|
||||
selectorName?: string;
|
||||
}
|
||||
|
||||
export const EXAMPLE_COMPONENTS: {[key: string]: LiveExample} = ${exampleComponents};
|
||||
export const EXAMPLE_LIST = ${exampleList}
|
||||
|
||||
@NgModule({
|
||||
declarations: EXAMPLE_LIST,
|
||||
entryComponents: EXAMPLE_LIST,
|
||||
imports: [
|
||||
ExampleMaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class ExampleModule { }
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {parseExampleFile} from './parse-example-file';
|
||||
|
||||
interface ExampleMetadata {
|
||||
component: string;
|
||||
sourcePath: string;
|
||||
id: string;
|
||||
title: string;
|
||||
additionalComponents: string[];
|
||||
additionalFiles: string[];
|
||||
selectorName: string[];
|
||||
}
|
||||
|
||||
/** Build ES module import statements for the given example metadata. */
|
||||
function buildImportsTemplate(data: ExampleMetadata): string {
|
||||
const components = data.additionalComponents.concat(data.component);
|
||||
const relativeSrcPath = data.sourcePath.replace(/\\/g, '/').replace('.ts', '');
|
||||
|
||||
return `import {${components.join(',')}} from './${relativeSrcPath}';`;
|
||||
}
|
||||
|
||||
/** Inlines the example module template with the specified parsed data. */
|
||||
function inlineExampleModuleTemplate(parsedData: ExampleMetadata[]): string {
|
||||
const exampleImports = parsedData.map(m => buildImportsTemplate(m)).join('\n');
|
||||
const exampleList = parsedData.reduce((result, data) => {
|
||||
return result.concat(data.component).concat(data.additionalComponents);
|
||||
}, [] as string[]).join(',');
|
||||
|
||||
const exampleComponents = parsedData.reduce((result, data) => {
|
||||
result[data.id] = {
|
||||
title: data.title,
|
||||
component: data.component,
|
||||
additionalFiles: data.additionalFiles,
|
||||
selectorName: data.selectorName.join(', '),
|
||||
};
|
||||
|
||||
return result;
|
||||
}, {} as any);
|
||||
|
||||
return fs.readFileSync(require.resolve('./example-module.template'), 'utf8')
|
||||
.replace('${exampleImports}', exampleImports)
|
||||
.replace('${exampleComponents}', JSON.stringify(exampleComponents))
|
||||
.replace('${exampleList}', `[${exampleList}]`);
|
||||
}
|
||||
|
||||
/** Converts a given camel-cased string to a dash-cased string. */
|
||||
function convertToDashCase(name: string): string {
|
||||
name = name.replace(/[A-Z]/g, ' $&');
|
||||
name = name.toLowerCase().trim();
|
||||
return name.split(' ').join('-');
|
||||
}
|
||||
|
||||
/** Collects the metadata of the given source files by parsing the given TypeScript files. */
|
||||
function collectExampleMetadata(sourceFiles: string[], baseFile: string): ExampleMetadata[] {
|
||||
const exampleMetadata: ExampleMetadata[] = [];
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
|
||||
const {primaryComponent, secondaryComponents} = parseExampleFile(sourceFile, sourceContent);
|
||||
|
||||
if (primaryComponent) {
|
||||
// Generate a unique id for the component by converting the class name to dash-case.
|
||||
const exampleId = convertToDashCase(primaryComponent.component.replace('Example', ''));
|
||||
const example: ExampleMetadata = {
|
||||
sourcePath: path.relative(baseFile, sourceFile),
|
||||
id: exampleId,
|
||||
component: primaryComponent.component,
|
||||
title: primaryComponent.title.trim(),
|
||||
additionalComponents: [],
|
||||
additionalFiles: [],
|
||||
selectorName: []
|
||||
};
|
||||
|
||||
if (secondaryComponents.length) {
|
||||
example.selectorName.push(example.component);
|
||||
|
||||
for (const meta of secondaryComponents) {
|
||||
example.additionalComponents.push(meta.component);
|
||||
|
||||
if (meta.templateUrl) {
|
||||
example.additionalFiles.push(meta.templateUrl);
|
||||
}
|
||||
|
||||
if (meta.styleUrls) {
|
||||
example.additionalFiles.push(...meta.styleUrls);
|
||||
}
|
||||
|
||||
example.selectorName.push(meta.component);
|
||||
}
|
||||
}
|
||||
|
||||
exampleMetadata.push(example);
|
||||
}
|
||||
}
|
||||
|
||||
return exampleMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the example module from the given source files and writes it to a specified output
|
||||
* file.
|
||||
*/
|
||||
export function generateExampleModule(sourceFiles: string[], outputFile: string,
|
||||
baseDir: string = path.dirname(outputFile)) {
|
||||
const results = collectExampleMetadata(sourceFiles, baseDir);
|
||||
const generatedModuleFile = inlineExampleModuleTemplate(results);
|
||||
|
||||
fs.writeFileSync(outputFile, generatedModuleFile);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
interface ParsedMetadata {
|
||||
primary: boolean;
|
||||
component: string;
|
||||
title: string;
|
||||
templateUrl: string;
|
||||
styleUrls: string[];
|
||||
}
|
||||
|
||||
interface ParsedMetadataResults {
|
||||
primaryComponent: ParsedMetadata;
|
||||
secondaryComponents: ParsedMetadata[];
|
||||
}
|
||||
|
||||
/** Parse the AST of the given source file and collect Angular component metadata. */
|
||||
export function parseExampleFile(fileName: string, content: string): ParsedMetadataResults {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false);
|
||||
const metas: any[] = [];
|
||||
|
||||
const visitNode = (node: any): void => {
|
||||
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
|
||||
const meta: any = {
|
||||
component: node.name.text
|
||||
};
|
||||
|
||||
if (node.jsDoc && node.jsDoc.length) {
|
||||
for (const doc of node.jsDoc) {
|
||||
if (doc.tags && doc.tags.length) {
|
||||
for (const tag of doc.tags) {
|
||||
const tagValue = tag.comment;
|
||||
const tagName = tag.tagName.text;
|
||||
if (tagName === 'title') {
|
||||
meta.title = tagValue;
|
||||
meta.primary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.decorators && node.decorators.length) {
|
||||
for (const decorator of node.decorators) {
|
||||
if (decorator.expression.expression.text === 'Component') {
|
||||
for (const arg of decorator.expression.arguments) {
|
||||
for (const prop of arg.properties) {
|
||||
const propName = prop.name.text;
|
||||
|
||||
// Since additional files can be also stylesheets, we need to properly parse
|
||||
// the styleUrls metadata property.
|
||||
if (propName === 'styleUrls' && ts.isArrayLiteralExpression(prop.initializer)) {
|
||||
meta[propName] = prop.initializer.elements
|
||||
.map((literal: ts.StringLiteral) => literal.text);
|
||||
} else {
|
||||
meta[propName] = prop.initializer.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metas.push(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visitNode);
|
||||
};
|
||||
|
||||
visitNode(sourceFile);
|
||||
|
||||
return {
|
||||
primaryComponent: metas.find(m => m.primary),
|
||||
secondaryComponents: metas.filter(m => !m.primary)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"bazelOptions": {
|
||||
"suppressTsconfigOverrideWarnings": true
|
||||
}
|
||||
}
|
||||
@@ -1,256 +1,20 @@
|
||||
import {task} from 'gulp';
|
||||
import {sync as glob} from 'glob';
|
||||
import {task} from 'gulp';
|
||||
import {buildConfig} from 'material2-build-tools';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import {generateExampleModule} from '../../example-module/generate-example-module';
|
||||
|
||||
const {packagesDir} = buildConfig;
|
||||
|
||||
interface ExampleMetadata {
|
||||
component: string;
|
||||
sourcePath: string;
|
||||
id: string;
|
||||
title: string;
|
||||
additionalComponents: string[];
|
||||
additionalFiles: string[];
|
||||
selectorName: string[];
|
||||
}
|
||||
|
||||
interface ParsedMetadata {
|
||||
primary: boolean;
|
||||
component: string;
|
||||
title: string;
|
||||
templateUrl: string;
|
||||
styleUrls: string[];
|
||||
}
|
||||
|
||||
interface ParsedMetadataResults {
|
||||
primaryComponent: ParsedMetadata;
|
||||
secondaryComponents: ParsedMetadata[];
|
||||
}
|
||||
|
||||
/** Path to find the examples */
|
||||
const examplesPath = path.join(packagesDir, 'material-examples');
|
||||
|
||||
/** Output path of the module that is being created */
|
||||
const outputModuleFilename = path.join(examplesPath, 'example-module.ts');
|
||||
|
||||
/** Build ES module import statements for the examples. */
|
||||
function buildImportsTemplate(metadata: ExampleMetadata): string {
|
||||
const components = metadata.additionalComponents.concat(metadata.component);
|
||||
|
||||
// Create a relative path to the source file of the current example.
|
||||
// The relative path will be used inside of a TypeScript import statement.
|
||||
const relativeSrcPath = path
|
||||
.relative(examplesPath, metadata.sourcePath)
|
||||
.replace(/\\/g, '/')
|
||||
.replace('.ts', '');
|
||||
|
||||
return `import {${components.join(',')}} from './${relativeSrcPath}';
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the examples metadata including title, component, etc.
|
||||
*/
|
||||
function buildExamplesTemplate(metadata: ExampleMetadata): string {
|
||||
const fields = [
|
||||
`title: '${metadata.title.trim()}'`,
|
||||
`component: ${metadata.component}`,
|
||||
];
|
||||
|
||||
// if no additional files or selectors were provided,
|
||||
// return null since we don't care about if these were not found
|
||||
if (metadata.additionalFiles.length) {
|
||||
fields.push(`additionalFiles: ${JSON.stringify(metadata.additionalFiles)}`);
|
||||
}
|
||||
|
||||
if (metadata.selectorName.length) {
|
||||
fields.push(`selectorName: '${metadata.selectorName.join(', ')}'`);
|
||||
}
|
||||
|
||||
const data = '\n' + fields.map(field => ' ' + field).join(',\n');
|
||||
|
||||
return `'${metadata.id}': {${data}
|
||||
},
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the list of components template
|
||||
*/
|
||||
function buildListTemplate(metadata: ExampleMetadata): string {
|
||||
const components = metadata.additionalComponents.concat(metadata.component);
|
||||
return `${components.join(',')},
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the template for the examples module
|
||||
*/
|
||||
function generateExampleNgModule(extractedMetadata: ExampleMetadata[]): string {
|
||||
return `
|
||||
/* tslint:disable */
|
||||
/** DO NOT MANUALLY EDIT THIS FILE, IT IS GENERATED VIA GULP 'build-examples-module' */
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ExampleMaterialModule} from './material-module';
|
||||
${extractedMetadata.map(r => buildImportsTemplate(r)).join('').trim()}
|
||||
|
||||
export interface LiveExample {
|
||||
title: string;
|
||||
component: any;
|
||||
additionalFiles?: string[];
|
||||
selectorName?: string;
|
||||
}
|
||||
|
||||
export const EXAMPLE_COMPONENTS: {[key: string]: LiveExample} = {
|
||||
${extractedMetadata.map(r => buildExamplesTemplate(r)).join('').trim()}
|
||||
};
|
||||
|
||||
export const EXAMPLE_LIST = [
|
||||
${extractedMetadata.map(r => buildListTemplate(r)).join('').trim()}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: EXAMPLE_LIST,
|
||||
entryComponents: EXAMPLE_LIST,
|
||||
imports: [
|
||||
ExampleMaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule
|
||||
]
|
||||
})
|
||||
export class ExampleModule { }
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string that is a camel or pascal case,
|
||||
* this function will convert to dash case.
|
||||
*/
|
||||
function convertToDashCase(name: string): string {
|
||||
name = name.replace(/[A-Z]/g, ' $&');
|
||||
name = name.toLowerCase().trim();
|
||||
return name.split(' ').join('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the AST of a file and get metadata about it
|
||||
*/
|
||||
function parseExampleMetadata(fileName: string, sourceContent: string): ParsedMetadataResults {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
fileName, sourceContent, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
||||
|
||||
const metas: any[] = [];
|
||||
|
||||
const visit = (node: any): void => {
|
||||
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
|
||||
const meta: any = {
|
||||
component: node.name.text
|
||||
};
|
||||
|
||||
if (node.jsDoc && node.jsDoc.length) {
|
||||
for (const doc of node.jsDoc) {
|
||||
if (doc.tags && doc.tags.length) {
|
||||
for (const tag of doc.tags) {
|
||||
const tagValue = tag.comment;
|
||||
const tagName = tag.tagName.text;
|
||||
if (tagName === 'title') {
|
||||
meta.title = tagValue;
|
||||
meta.primary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.decorators && node.decorators.length) {
|
||||
for (const decorator of node.decorators) {
|
||||
if (decorator.expression.expression.text === 'Component') {
|
||||
for (const arg of decorator.expression.arguments) {
|
||||
for (const prop of arg.properties) {
|
||||
const propName = prop.name.text;
|
||||
|
||||
// Since additional files can be also stylesheets, we need to properly parse
|
||||
// the styleUrls metadata property.
|
||||
if (propName === 'styleUrls' && ts.isArrayLiteralExpression(prop.initializer)) {
|
||||
meta[propName] = prop.initializer.elements
|
||||
.map((literal: ts.StringLiteral) => literal.text);
|
||||
} else {
|
||||
meta[propName] = prop.initializer.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metas.push(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
|
||||
return {
|
||||
primaryComponent: metas.find(m => m.primary),
|
||||
secondaryComponents: metas.filter(m => !m.primary)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the examples module and metadata
|
||||
*/
|
||||
task('build-examples-module', () => {
|
||||
const results: ExampleMetadata[] = [];
|
||||
const matchedFiles = glob(path.join(examplesPath, '**/*.ts'));
|
||||
|
||||
for (const sourcePath of matchedFiles) {
|
||||
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
||||
const {primaryComponent, secondaryComponents} =
|
||||
parseExampleMetadata(sourcePath, sourceContent);
|
||||
|
||||
if (primaryComponent) {
|
||||
// Generate a unique id for the component by converting the class name to dash-case.
|
||||
const id = convertToDashCase(primaryComponent.component.replace('Example', ''));
|
||||
|
||||
const example: ExampleMetadata = {
|
||||
sourcePath,
|
||||
id,
|
||||
component: primaryComponent.component,
|
||||
title: primaryComponent.title,
|
||||
additionalComponents: [],
|
||||
additionalFiles: [],
|
||||
selectorName: []
|
||||
};
|
||||
|
||||
if (secondaryComponents.length) {
|
||||
example.selectorName.push(example.component);
|
||||
|
||||
for (const meta of secondaryComponents) {
|
||||
example.additionalComponents.push(meta.component);
|
||||
|
||||
if (meta.templateUrl) {
|
||||
example.additionalFiles.push(meta.templateUrl);
|
||||
}
|
||||
|
||||
if (meta.styleUrls) {
|
||||
example.additionalFiles.push(...meta.styleUrls);
|
||||
}
|
||||
|
||||
example.selectorName.push(meta.component);
|
||||
}
|
||||
}
|
||||
|
||||
results.push(example);
|
||||
}
|
||||
}
|
||||
|
||||
const generatedModuleFile = generateExampleNgModule(results);
|
||||
fs.writeFileSync(outputModuleFilename, generatedModuleFile);
|
||||
generateExampleModule(glob(path.join(examplesPath, '**/*.ts')), outputModuleFilename);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user