Skip to content

build: build material-examples with bazel #13932

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/bazel-tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
51 changes: 51 additions & 0 deletions src/material-examples/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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,
)

2 changes: 1 addition & 1 deletion src/material-examples/tsconfig-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
// Unset options inherited from tsconfig-build
"annotateForClosureCompiler": false,
"flatModuleOutFile": null,
"flatModuleId": null,
"flatModuleId": null
},
"include": [
"**/*.spec.ts",
Expand Down
25 changes: 25 additions & 0 deletions tools/example-module/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
16 changes: 16 additions & 0 deletions tools/example-module/bazel-bin.ts
Original file line number Diff line number Diff line change
@@ -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);
}
36 changes: 36 additions & 0 deletions tools/example-module/example-module.template
Original file line number Diff line number Diff line change
@@ -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 { }
110 changes: 110 additions & 0 deletions tools/example-module/generate-example-module.ts
Original file line number Diff line number Diff line change
@@ -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);
}
75 changes: 75 additions & 0 deletions tools/example-module/parse-example-file.ts
Original file line number Diff line number Diff line change
@@ -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)
};
}
12 changes: 12 additions & 0 deletions tools/example-module/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"lib": ["es2015"],
"module": "commonjs",
"target": "es5",
"sourceMap": true,
"types": ["node"]
},
"bazelOptions": {
"suppressTsconfigOverrideWarnings": true
}
}
Loading