diff --git a/src/bazel-tsconfig-build.json b/src/bazel-tsconfig-build.json index 57d7fe3333a..efddac0061a 100644 --- a/src/bazel-tsconfig-build.json +++ b/src/bazel-tsconfig-build.json @@ -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 } } diff --git a/src/material-examples/BUILD.bazel b/src/material-examples/BUILD.bazel new file mode 100644 index 00000000000..171fdac19c7 --- /dev/null +++ b/src/material-examples/BUILD.bazel @@ -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, +) + diff --git a/src/material-examples/tsconfig-tests.json b/src/material-examples/tsconfig-tests.json index 6b44279145f..04efc287495 100644 --- a/src/material-examples/tsconfig-tests.json +++ b/src/material-examples/tsconfig-tests.json @@ -18,7 +18,7 @@ // Unset options inherited from tsconfig-build "annotateForClosureCompiler": false, "flatModuleOutFile": null, - "flatModuleId": null, + "flatModuleId": null }, "include": [ "**/*.spec.ts", diff --git a/tools/example-module/BUILD.bazel b/tools/example-module/BUILD.bazel new file mode 100644 index 00000000000..fc38693aebd --- /dev/null +++ b/tools/example-module/BUILD.bazel @@ -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", + ], +) diff --git a/tools/example-module/bazel-bin.ts b/tools/example-module/bazel-bin.ts new file mode 100644 index 00000000000..a9959799a80 --- /dev/null +++ b/tools/example-module/bazel-bin.ts @@ -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); +} diff --git a/tools/example-module/example-module.template b/tools/example-module/example-module.template new file mode 100644 index 00000000000..e932f664ddc --- /dev/null +++ b/tools/example-module/example-module.template @@ -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 { } diff --git a/tools/example-module/generate-example-module.ts b/tools/example-module/generate-example-module.ts new file mode 100644 index 00000000000..ae957e8d73c --- /dev/null +++ b/tools/example-module/generate-example-module.ts @@ -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); +} diff --git a/tools/example-module/parse-example-file.ts b/tools/example-module/parse-example-file.ts new file mode 100644 index 00000000000..7d108a98844 --- /dev/null +++ b/tools/example-module/parse-example-file.ts @@ -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) + }; +} diff --git a/tools/example-module/tsconfig.json b/tools/example-module/tsconfig.json new file mode 100644 index 00000000000..13dba047bbc --- /dev/null +++ b/tools/example-module/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "module": "commonjs", + "target": "es5", + "sourceMap": true, + "types": ["node"] + }, + "bazelOptions": { + "suppressTsconfigOverrideWarnings": true + } +} diff --git a/tools/gulp/tasks/example-module.ts b/tools/gulp/tasks/example-module.ts index d76b35a0f39..bbc8711f407 100644 --- a/tools/gulp/tasks/example-module.ts +++ b/tools/gulp/tasks/example-module.ts @@ -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); });