Skip to content

Commit c64503e

Browse files
devversionjelbourn
authored andcommitted
build: build material-examples with bazel (#13932)
This includes a `genrule` to create the generated `ExampleModule`
1 parent 150c964 commit c64503e

10 files changed

+334
-240
lines changed

src/bazel-tsconfig-build.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@
2424
"lib": ["es2015", "dom"],
2525
"skipLibCheck": true,
2626
"types": ["tslib"]
27+
},
28+
"bazelOptions": {
29+
// Note: We can remove this once we fully switched away from Gulp. Currently we still set
30+
// some options here just in favor of the standard tsconfig's which extending this one.
31+
"suppressTsconfigOverrideWarnings": true
2732
}
2833
}

src/material-examples/BUILD.bazel

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package(default_visibility=["//visibility:public"])
2+
3+
load("@angular//:index.bzl", "ng_package")
4+
load("//:packages.bzl", "CDK_TARGETS", "MATERIAL_TARGETS", "ROLLUP_GLOBALS")
5+
load("//tools:defaults.bzl", "ng_module")
6+
7+
ng_module(
8+
name = "examples",
9+
srcs = glob(["**/*.ts"]) + [":example-module"],
10+
module_name = "@angular/material-examples",
11+
assets = glob(["**/*.html", "**/*.css"]),
12+
deps = [
13+
"@angular//packages/common",
14+
"@angular//packages/core",
15+
"@angular//packages/forms",
16+
"@npm//moment",
17+
"//src/material-moment-adapter",
18+
] + CDK_TARGETS + MATERIAL_TARGETS,
19+
# Specify the tsconfig that is also used by Gulp. We need to explicitly use this tsconfig
20+
# because in order to import Moment with TypeScript, some specific options need to be set.
21+
tsconfig = ":tsconfig-build.json",
22+
)
23+
24+
ng_package(
25+
name = "npm_package",
26+
srcs = ["package.json"],
27+
entry_point = "src/material-examples/public_api.js",
28+
globals = ROLLUP_GLOBALS,
29+
deps = [":examples"],
30+
# TODO(devversion): re-enable once we have set up the proper compiler for the ng_package
31+
tags = ["manual"],
32+
)
33+
34+
genrule(
35+
name = "example-module",
36+
srcs = glob(["**/*.ts"]),
37+
outs = ["example-module.ts"],
38+
cmd = """
39+
# As a workaround for https://github.com/bazelbuild/rules_nodejs/issues/404, we pass the
40+
# data to the Bazel entry-point through environment variables.
41+
export _SOURCE_FILES="$(SRCS)"
42+
export _OUTPUT_FILE="$@"
43+
export _BASE_DIR="$$(dirname $(location //src/material-examples:index.ts))"
44+
45+
# Run the bazel entry-point for generating the example module.
46+
./$(location //tools/example-module:bazel-bin)
47+
""",
48+
tools = ["//tools/example-module:bazel-bin"],
49+
output_to_bindir = True,
50+
)
51+

src/material-examples/tsconfig-tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
// Unset options inherited from tsconfig-build
1919
"annotateForClosureCompiler": false,
2020
"flatModuleOutFile": null,
21-
"flatModuleId": null,
21+
"flatModuleId": null
2222
},
2323
"include": [
2424
"**/*.spec.ts",

tools/example-module/BUILD.bazel

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package(default_visibility=["//visibility:public"])
2+
3+
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
4+
load("//tools:defaults.bzl", "ts_library")
5+
6+
ts_library(
7+
name = "example-module-lib",
8+
srcs = glob(["**/*.ts"]),
9+
deps = [
10+
"@npm//@types/node",
11+
"@npm//typescript",
12+
],
13+
tsconfig = ":tsconfig.json",
14+
)
15+
16+
nodejs_binary(
17+
name = "bazel-bin",
18+
entry_point = "angular_material/tools/example-module/bazel-bin.js",
19+
data = [
20+
"@npm//typescript",
21+
"@npm//source-map-support",
22+
":example-module-lib",
23+
":example-module.template",
24+
],
25+
)

tools/example-module/bazel-bin.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {generateExampleModule} from './generate-example-module';
2+
3+
/**
4+
* Entry point for the Bazel NodeJS target. Usually this would be a more generic CLI, but due to
5+
* Bazel not being able to handle a lot of files on Windows (with emulated Bash), we need to
6+
* read the arguments through environment variables which are handled better.
7+
*
8+
* - https://github.com/bazelbuild/rules_nodejs/issues/404
9+
* - https://github.com/bazelbuild/bazel/issues/3636
10+
*/
11+
12+
if (require.main === module) {
13+
const {_SOURCE_FILES, _OUTPUT_FILE, _BASE_DIR} = process.env;
14+
15+
generateExampleModule(_SOURCE_FILES.split(' '), _OUTPUT_FILE, _BASE_DIR);
16+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* tslint:disable */
2+
3+
/**
4+
******************************************************************************
5+
* DO NOT MANUALLY EDIT THIS FILE. THIS FILE IS AUTOMATICALLY GENERATED.
6+
******************************************************************************
7+
*/
8+
9+
import {NgModule} from '@angular/core';
10+
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
11+
import {CommonModule} from '@angular/common';
12+
import {ExampleMaterialModule} from './material-module';
13+
14+
${exampleImports}
15+
16+
export interface LiveExample {
17+
title: string;
18+
component: any;
19+
additionalFiles?: string[];
20+
selectorName?: string;
21+
}
22+
23+
export const EXAMPLE_COMPONENTS: {[key: string]: LiveExample} = ${exampleComponents};
24+
export const EXAMPLE_LIST = ${exampleList}
25+
26+
@NgModule({
27+
declarations: EXAMPLE_LIST,
28+
entryComponents: EXAMPLE_LIST,
29+
imports: [
30+
ExampleMaterialModule,
31+
FormsModule,
32+
ReactiveFormsModule,
33+
CommonModule
34+
]
35+
})
36+
export class ExampleModule { }
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import {parseExampleFile} from './parse-example-file';
4+
5+
interface ExampleMetadata {
6+
component: string;
7+
sourcePath: string;
8+
id: string;
9+
title: string;
10+
additionalComponents: string[];
11+
additionalFiles: string[];
12+
selectorName: string[];
13+
}
14+
15+
/** Build ES module import statements for the given example metadata. */
16+
function buildImportsTemplate(data: ExampleMetadata): string {
17+
const components = data.additionalComponents.concat(data.component);
18+
const relativeSrcPath = data.sourcePath.replace(/\\/g, '/').replace('.ts', '');
19+
20+
return `import {${components.join(',')}} from './${relativeSrcPath}';`;
21+
}
22+
23+
/** Inlines the example module template with the specified parsed data. */
24+
function inlineExampleModuleTemplate(parsedData: ExampleMetadata[]): string {
25+
const exampleImports = parsedData.map(m => buildImportsTemplate(m)).join('\n');
26+
const exampleList = parsedData.reduce((result, data) => {
27+
return result.concat(data.component).concat(data.additionalComponents);
28+
}, [] as string[]).join(',');
29+
30+
const exampleComponents = parsedData.reduce((result, data) => {
31+
result[data.id] = {
32+
title: data.title,
33+
component: data.component,
34+
additionalFiles: data.additionalFiles,
35+
selectorName: data.selectorName.join(', '),
36+
};
37+
38+
return result;
39+
}, {} as any);
40+
41+
return fs.readFileSync(require.resolve('./example-module.template'), 'utf8')
42+
.replace('${exampleImports}', exampleImports)
43+
.replace('${exampleComponents}', JSON.stringify(exampleComponents))
44+
.replace('${exampleList}', `[${exampleList}]`);
45+
}
46+
47+
/** Converts a given camel-cased string to a dash-cased string. */
48+
function convertToDashCase(name: string): string {
49+
name = name.replace(/[A-Z]/g, ' $&');
50+
name = name.toLowerCase().trim();
51+
return name.split(' ').join('-');
52+
}
53+
54+
/** Collects the metadata of the given source files by parsing the given TypeScript files. */
55+
function collectExampleMetadata(sourceFiles: string[], baseFile: string): ExampleMetadata[] {
56+
const exampleMetadata: ExampleMetadata[] = [];
57+
58+
for (const sourceFile of sourceFiles) {
59+
const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
60+
const {primaryComponent, secondaryComponents} = parseExampleFile(sourceFile, sourceContent);
61+
62+
if (primaryComponent) {
63+
// Generate a unique id for the component by converting the class name to dash-case.
64+
const exampleId = convertToDashCase(primaryComponent.component.replace('Example', ''));
65+
const example: ExampleMetadata = {
66+
sourcePath: path.relative(baseFile, sourceFile),
67+
id: exampleId,
68+
component: primaryComponent.component,
69+
title: primaryComponent.title.trim(),
70+
additionalComponents: [],
71+
additionalFiles: [],
72+
selectorName: []
73+
};
74+
75+
if (secondaryComponents.length) {
76+
example.selectorName.push(example.component);
77+
78+
for (const meta of secondaryComponents) {
79+
example.additionalComponents.push(meta.component);
80+
81+
if (meta.templateUrl) {
82+
example.additionalFiles.push(meta.templateUrl);
83+
}
84+
85+
if (meta.styleUrls) {
86+
example.additionalFiles.push(...meta.styleUrls);
87+
}
88+
89+
example.selectorName.push(meta.component);
90+
}
91+
}
92+
93+
exampleMetadata.push(example);
94+
}
95+
}
96+
97+
return exampleMetadata;
98+
}
99+
100+
/**
101+
* Generates the example module from the given source files and writes it to a specified output
102+
* file.
103+
*/
104+
export function generateExampleModule(sourceFiles: string[], outputFile: string,
105+
baseDir: string = path.dirname(outputFile)) {
106+
const results = collectExampleMetadata(sourceFiles, baseDir);
107+
const generatedModuleFile = inlineExampleModuleTemplate(results);
108+
109+
fs.writeFileSync(outputFile, generatedModuleFile);
110+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as ts from 'typescript';
2+
3+
interface ParsedMetadata {
4+
primary: boolean;
5+
component: string;
6+
title: string;
7+
templateUrl: string;
8+
styleUrls: string[];
9+
}
10+
11+
interface ParsedMetadataResults {
12+
primaryComponent: ParsedMetadata;
13+
secondaryComponents: ParsedMetadata[];
14+
}
15+
16+
/** Parse the AST of the given source file and collect Angular component metadata. */
17+
export function parseExampleFile(fileName: string, content: string): ParsedMetadataResults {
18+
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, false);
19+
const metas: any[] = [];
20+
21+
const visitNode = (node: any): void => {
22+
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
23+
const meta: any = {
24+
component: node.name.text
25+
};
26+
27+
if (node.jsDoc && node.jsDoc.length) {
28+
for (const doc of node.jsDoc) {
29+
if (doc.tags && doc.tags.length) {
30+
for (const tag of doc.tags) {
31+
const tagValue = tag.comment;
32+
const tagName = tag.tagName.text;
33+
if (tagName === 'title') {
34+
meta.title = tagValue;
35+
meta.primary = true;
36+
}
37+
}
38+
}
39+
}
40+
}
41+
42+
if (node.decorators && node.decorators.length) {
43+
for (const decorator of node.decorators) {
44+
if (decorator.expression.expression.text === 'Component') {
45+
for (const arg of decorator.expression.arguments) {
46+
for (const prop of arg.properties) {
47+
const propName = prop.name.text;
48+
49+
// Since additional files can be also stylesheets, we need to properly parse
50+
// the styleUrls metadata property.
51+
if (propName === 'styleUrls' && ts.isArrayLiteralExpression(prop.initializer)) {
52+
meta[propName] = prop.initializer.elements
53+
.map((literal: ts.StringLiteral) => literal.text);
54+
} else {
55+
meta[propName] = prop.initializer.text;
56+
}
57+
}
58+
}
59+
60+
metas.push(meta);
61+
}
62+
}
63+
}
64+
}
65+
66+
ts.forEachChild(node, visitNode);
67+
};
68+
69+
visitNode(sourceFile);
70+
71+
return {
72+
primaryComponent: metas.find(m => m.primary),
73+
secondaryComponents: metas.filter(m => !m.primary)
74+
};
75+
}

tools/example-module/tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"lib": ["es2015"],
4+
"module": "commonjs",
5+
"target": "es5",
6+
"sourceMap": true,
7+
"types": ["node"]
8+
},
9+
"bazelOptions": {
10+
"suppressTsconfigOverrideWarnings": true
11+
}
12+
}

0 commit comments

Comments
 (0)