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
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user