ipsos-components
Version:
Material Design components for Angular
236 lines (199 loc) • 6.97 kB
text/typescript
import {task} from 'gulp';
import {sync as glob} from 'glob';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {buildConfig} from 'material2-build-tools';
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;
}
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 {
// if no additional files or selectors were provided,
// return null since we don't care about if these were not found
const additionalFiles = metadata.additionalFiles.length ?
JSON.stringify(metadata.additionalFiles) : 'null';
const selectorName = metadata.selectorName.length ?
`'${metadata.selectorName.join(', ')}'` : 'null';
return `'${metadata.id}': {
title: '${metadata.title}',
component: ${metadata.component},
additionalFiles: ${additionalFiles},
selectorName: ${selectorName}
},
`;
}
/**
* 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';
export interface LiveExample {
title: string;
component: any;
additionalFiles?: string[];
selectorName?: string;
}
${extractedMetadata.map(r => buildImportsTemplate(r)).join('').trim()}
export const EXAMPLE_COMPONENTS = {
${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 name = prop.name.text;
const value = prop.initializer.text;
meta[name] = value;
}
}
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);
example.additionalFiles.push(meta.templateUrl);
example.selectorName.push(meta.component);
}
}
results.push(example);
}
}
const generatedModuleFile = generateExampleNgModule(results);
fs.writeFileSync(outputModuleFilename, generatedModuleFile);
});