Skip to content

Commit

Permalink
feat(schematics): add fonts & dynamic styles
Browse files Browse the repository at this point in the history
  • Loading branch information
Enlcxx committed Jan 7, 2019
1 parent 5ad0197 commit d2bb27e
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 9 deletions.
33 changes: 33 additions & 0 deletions src/lib/schematics/ng-add/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import { Schema } from './schema';
import { getProjectTargets, targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';

/** Adds the Roboto & Material Icons fonts to the index HTML file. */
export function addFontsToIndex(options: Schema): (host: Tree) => Tree {
return (host: Tree) => {
const projectTargets = getProjectTargets(host, options.project);

if (!projectTargets.build) {
throw targetBuildNotFoundError();
}

const indexPath = projectTargets.build.options.index;
if (indexPath === undefined) {
return host;
}

const buffer = host.read(indexPath);

if (buffer === null) {
throw new SchematicsException(`Could not read index file: ${indexPath}`);
}

const fonts = '<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500|Material+Icons" rel="stylesheet">';
const htmlText = buffer.toString().replace(/([\s]+)?\<\/head\>([\s]+)?\<body\>/, (match, token) => {
return match.replace(token, `${token}\n ${fonts}\n`);
});
host.overwrite(indexPath, htmlText);

return host;
};
}
35 changes: 26 additions & 9 deletions src/lib/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,23 @@ import { join } from 'path';
import { addHammerJsToMain } from './gestures';
import { Schema } from './schema';
import { setUpAppModule } from './set-up';
import { addFontsToIndex } from './fonts';
import { getAppComponentPath } from '../utils/get-app-component-path';
import { setUpStyles } from './styles';

const AUI_VERSION = require(`@alyle/ui/package.json`).version;
const ANGULAR_CORE_VERSION = require(join(process.cwd(), 'package.json')).dependencies['@angular/core'];
let AUI_VERSION: string;
try {
AUI_VERSION = require(`@alyle/ui/package.json`).version;
} catch (error) {
AUI_VERSION = '*';
}

let ANGULAR_CORE_VERSION: string;
try {
ANGULAR_CORE_VERSION = require(join(process.cwd(), 'package.json')).dependencies['@angular/core'];
} catch (error) {
ANGULAR_CORE_VERSION = '*';
}
const HAMMERJS_VERSION = '^2.0.8';
const CHROMA_JS_VERSION = '^1.3.6';

Expand All @@ -29,12 +43,13 @@ function addPkg(host: Tree, pkgName: string, version: string) {
}
}

function installPkgs(_options: Schema): Rule {
function installPkgs(options: Schema): Rule {
return (host: Tree, _context: SchematicContext) => {
_context.logger.debug('installPkgs');
addPkg(host, '@angular/animations', ANGULAR_CORE_VERSION);
addPkg(host, '@alyle/ui', `^${AUI_VERSION}`);
addPkg(host, 'chroma-js', CHROMA_JS_VERSION);
if (_options.gestures) {
if (options.gestures) {
addPkg(host, 'hammerjs', HAMMERJS_VERSION);
}
_context.addTask(new NodePackageInstallTask());
Expand All @@ -43,10 +58,12 @@ function installPkgs(_options: Schema): Rule {

// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function ngAdd(_options: Schema): Rule {
return chain([
addHammerJsToMain(_options),
setUpAppModule(_options),
installPkgs(_options)
export function ngAdd(options: Schema): Rule {
return (host: Tree) => chain([
addHammerJsToMain(options),
setUpAppModule(options),
addFontsToIndex(options),
setUpStyles(options, getAppComponentPath(host, options)),
installPkgs(options)
]);
}
105 changes: 105 additions & 0 deletions src/lib/schematics/ng-add/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import * as ts from '@schematics/angular/node_modules/typescript';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import { Schema } from './schema';
import { getProjectTargets, targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';
import { addImport, getTsSourceFile, prettierConstructorParameters } from '../utils/ast';
import { findNodes } from '@schematics/angular/utility/ast-utils';

/** Adds the styles to the src/app/app.component.ts file. */
export function setUpStyles(options: Schema, filePath: string): (host: Tree) => Tree {
return (host: Tree) => {
const projectTargets = getProjectTargets(host, options.project);

if (!projectTargets.build) {
throw targetBuildNotFoundError();
}

const buffer = host.read(filePath);

if (buffer === null) {
throw new SchematicsException(`Could not read index file: ${filePath}`);
}

// add import style
addImport(host, filePath, ['LyTheme2', 'ThemeVariables'], '@alyle/ui');

let component = getComponentOrDirective(host, filePath);
const componentStartPos = component.decorators![0].pos;

const recorder = host.beginUpdate(filePath);
recorder.insertLeft(componentStartPos, `\n\nconst STYLES = (theme: ThemeVariables) => ({
'@global': {
body: {
backgroundColor: theme.background.default,
color: theme.text.default,
fontFamily: theme.typography.fontFamily,
margin: 0,
direction: theme.direction
}
}
});`);
host.commitUpdate(recorder);

component = getComponentOrDirective(host, filePath);

const hasConstructor = component.members
.some(prop => (prop.kind === ts.SyntaxKind.Constructor) && !!(prop as ts.ConstructorDeclaration).body);
let constructor: ts.ConstructorDeclaration;
let __recorder = host.beginUpdate(filePath);
const propertyValue = `\n readonly classes = this.theme.addStyleSheet(STYLES);\n`;
const constructorCall = ` constructor(\n private theme: LyTheme\n ) { }\n`;
const OpenBraceTokenPos = findNodes(component, ts.SyntaxKind.OpenBraceToken)
.filter(prop => prop.parent === component).map(prop => prop.end)[0];

__recorder.insertLeft(OpenBraceTokenPos, propertyValue);
host.commitUpdate(__recorder);
__recorder = host.beginUpdate(filePath);


component = getComponentOrDirective(host, filePath);

if (hasConstructor) {
constructor = getContructor(component);

const pos = findNodes(constructor, ts.SyntaxKind.OpenParenToken)
.filter(prop => prop.parent === constructor).map(prop => prop.end)[0];

if (constructor.parameters.length) {
__recorder.insertLeft(pos, `\n private theme: LyTheme2,\n `);
} else {
__recorder.insertLeft(pos, `\n private theme: LyTheme2\n `);
}
} else if (component.members.length) {
const latestPropertyDeclarationEnd = component.members
.filter(prop => prop.kind === ts.SyntaxKind.PropertyDeclaration)
.map(({ end }: ts.PropertyDeclaration) => end).reverse()[0];
__recorder.insertLeft(latestPropertyDeclarationEnd, `\n\n${constructorCall}`);
} else {
__recorder.insertLeft(OpenBraceTokenPos, constructorCall);
}
host.commitUpdate(__recorder);

component = getComponentOrDirective(host, filePath);
constructor = getContructor(component);

prettierConstructorParameters(host, filePath, constructor);

console.log(host.read(filePath)!.toString());
return host;
};
}

function getComponentOrDirective(host: Tree, filePath: string) {
const fileSource = getTsSourceFile(host, filePath);
return findNodes(fileSource, ts.SyntaxKind.ClassDeclaration, 1)
.filter(prop => prop.decorators)
.filter(prop => prop.decorators!.filter(
decorator => decorator.getText().startsWith('@Component') ||
decorator.getText().startsWith('@Directive')
))[0] as ts.ClassDeclaration;
}

function getContructor(componentOrDirective: ts.ClassDeclaration) {
return (componentOrDirective.members
.filter(prop => prop.kind === ts.SyntaxKind.Constructor)[0] as ts.ConstructorDeclaration);
}
73 changes: 73 additions & 0 deletions src/lib/schematics/utils/ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import * as ts from '@schematics/angular/node_modules/typescript';
import { isImported, insertImport } from '@schematics/angular/utility/ast-utils';
import { InsertChange } from '@schematics/angular/utility/change';

export function getTsSourceFile(host: Tree, path: string): ts.SourceFile {
const buffer = host.read(path);
if (!buffer) {
throw new SchematicsException(`Could not read file (${path}).`);
}
const content = buffer.toString();
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);

return source;
}

export function addImport(host: Tree, filePath: string, importModule: string | string[], path: string) {
let importModules: string[];
if (typeof importModule === 'string') {
importModules = [importModule];
} else {
importModules = importModule;
}
// add import theme
importModules.forEach((val) => {
const fileSource = getTsSourceFile(host, filePath);
const importPath = path;
if (!isImported(fileSource, val, importPath)) {
const change = insertImport
(fileSource, filePath, val, importPath);
if (change) {
const recorder = host.beginUpdate(filePath);
recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd);
host.commitUpdate(recorder);
}
}
});
}

export function prettierConstructorParameters(host: Tree,
filePath: string,
constructor: ts.ConstructorDeclaration): void {
const parenToken = constructor.getChildren()
.filter(prop => prop.kind === ts.SyntaxKind.OpenParenToken || prop.kind === ts.SyntaxKind.CloseParenToken);
// .map(prop => prop.getFullText().trim());
const buffer = host.read(filePath);

if (buffer === null) {
throw new SchematicsException(`Could not read index file: ${filePath}`);
}

const syntaxList = constructor.getChildren()
.filter(prop => prop.kind === ts.SyntaxKind.SyntaxList)[0];
const contructorBefore =
`${parenToken[0].getFullText()}${syntaxList.getFullText()}${parenToken[1].getFullText()}`;
const parameters = syntaxList.getChildren()
.filter(prop => prop.kind === ts.SyntaxKind.Parameter)
.map((parameter: ts.ParameterDeclaration, index) => {
const param = parameter.getText();
let comment = parameter.getFullText().replace(param, '').trim();
if (comment) {
comment += `\n`;
if (index === 0) {
comment += `\n${comment}`;
}
}
return `${comment}${param}`;
});
const parametersStr = parameters.join(`,\n${` `.repeat(14)}`);
const result =
`${parenToken[0].getFullText().trim()}${parametersStr}${parenToken[1].getFullText().trim()}`;
host.overwrite(filePath, buffer.toString().replace(contructorBefore, result));
}
42 changes: 42 additions & 0 deletions src/lib/schematics/utils/get-app-component-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Tree } from '@angular-devkit/schematics';
import { getProjectTargets, targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';
import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils';
import { getDecoratorMetadata, findNode } from '@schematics/angular/utility/ast-utils';
import * as ts from '@schematics/angular/node_modules/typescript';
import { dirname } from 'path';
import { normalize } from '@angular-devkit/core';
import { getTsSourceFile } from './ast';

export function getAppComponentPath(host: Tree, options: any) {
const projectTargets = getProjectTargets(host, options.project);
if (!projectTargets.build) {
throw targetBuildNotFoundError();
}
const mainPath = projectTargets.build.options.main;
const modulePath = getAppModulePath(host, mainPath);
const moduleSource = getTsSourceFile(host, modulePath);
const decoratorMetadata = getDecoratorMetadata(moduleSource, 'NgModule', '@angular/core');
const propertyName = 'bootstrap';
const { properties } = decoratorMetadata[0] as ts.ObjectLiteralExpression;
const property = properties
.filter(prop => prop.kind === ts.SyntaxKind.PropertyAssignment)
.filter((prop: ts.PropertyAssignment) => {
const name = prop.name;
switch (name.kind) {
case ts.SyntaxKind.Identifier:
return (name as ts.Identifier).getText() === propertyName;
case ts.SyntaxKind.StringLiteral:
return (name as ts.StringLiteral).text === propertyName;
}

return false;
})[0];
const bootstrapValue = ((property as ts.PropertyAssignment).initializer.getText()).split(/\[|\]|\,\s?/g).filter(s => s)[0];
const appComponentPath = moduleSource.statements
.filter(prop => prop.kind === ts.SyntaxKind.ImportDeclaration)
.filter(prop => findNode(prop, ts.SyntaxKind.ImportSpecifier, bootstrapValue))
.map(({ moduleSpecifier }: ts.ImportDeclaration) => (moduleSpecifier as ts.StringLiteral).text)[0];

const mainDir = dirname(modulePath);
return normalize(`/${mainDir}/${appComponentPath}.ts`);
}

0 comments on commit d2bb27e

Please sign in to comment.