Skip to content

Commit

Permalink
feat(schematics): support for schematics
Browse files Browse the repository at this point in the history
  • Loading branch information
Enlcxx committed Jan 3, 2019
1 parent 255b608 commit 340e8ef
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 19 deletions.
8 changes: 7 additions & 1 deletion src/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@
"url": "https://github.com/A-l-y-l-e/Alyle-UI/issues"
},
"homepage": "https://alyle-ui.firebaseapp.com/",
"dependencies": {
"@angular-devkit/core": "^7.2.0-rc.0 || ^7.2.0",
"@angular-devkit/schematics": "^7.2.0-rc.0 || ^7.2.0",
"@schematics/angular": "^7.2.0-rc.0 || ^7.2.0"
},
"peerDependencies": {
"@angular/cli": "^7.2.0-rc.0 || ^7.2.0",
"@angular/common": "^6.1.0 || ^7.0.0 || ^7.0.0-beta.0",
"@angular/core": "^6.1.0 || ^7.0.0 || ^7.0.0-beta.0",
"chroma-js": "^1.3.6",
Expand All @@ -36,7 +42,7 @@
"schematics": "./schematics/collection.json",
"ng-update": {
"packageGroup": [
"@@alyle/ui"
"@alyle/ui"
]
}
}
33 changes: 33 additions & 0 deletions src/lib/schematics/ng-add/gestures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Rule, Tree } from '@angular-devkit/schematics';
import { getWorkspace } from '@schematics/angular/utility/config';
import { Schema } from './schema';
import { getProjectFromWorkspace } from '../utils/get-project';
import { getProjectTargetOptions } from '../utils/get-project-target';

const hammerjsImportStatement = `import 'hammerjs';`;

/** Adds HammerJS to the main file of the specified Angular CLI project. */
export function addHammerJsToMain(options: Schema): Rule {
return (host: Tree) => {
const workspace = getWorkspace(host);
const project = getProjectFromWorkspace(workspace, options.project);
const mainFile = getProjectTargetOptions(project, 'build').main;

const recorder = host.beginUpdate(mainFile);
const buffer = host.read(mainFile);

if (!buffer) {
return console.error(`Could not read the project main file (${mainFile}). Please manually ` +
`import HammerJS in your main TypeScript file.`);
}

const fileContent = buffer.toString('utf8');

if (fileContent.includes(hammerjsImportStatement)) {
return console.log(`HammerJS is already imported in the project main file (${mainFile}).`);
}

recorder.insertRight(0, `${hammerjsImportStatement}\n`);
host.commitUpdate(recorder);
};
}
51 changes: 33 additions & 18 deletions src/lib/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { Rule, SchematicContext, Tree, chain } from '@angular-devkit/schematics';
import { addHammerJsToMain } from './gestures';
import { Schema } from './schema';
import { setUpAppModule } from './set-up';

const PKG = '@alyle/ui';
const AUI_VERSION = require(`${PKG}/package.json`).version;

// 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: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
if (tree.exists('package.json')) {
const sourceText = tree.read('package.json')!.toString('utf-8');
const json = JSON.parse(sourceText);
json.dependencies[PKG] = AUI_VERSION;
json.dependencies = sortObjectByKeys(json.dependencies);
}
_context.addTask(new NodePackageInstallTask());
};
}
const AUI_VERSION = require(`@alyle/ui/package.json`).version;
const ANGULAR_CORE_VERSION = require(`@angular/core/package.json`).version;
const HAMMERJS_VERSION = '^2.0.8';
const CHROMA_JS_VERSION = '^1.3.6';

/**
* Sorts the keys of the given object.
Expand All @@ -25,3 +15,28 @@ export function ngAdd(_options: any): Rule {
function sortObjectByKeys(obj: object) {
return Object.keys(obj).sort().reduce((result, key) => (result[key] = obj[key]) && result, {});
}

/** Add package */
function addPkg(tree: Tree, pkgName: string, version: string) {
if (tree.exists(pkgName)) {
const sourceText = tree.read(pkgName)!.toString('utf-8');
const json = JSON.parse(sourceText);
json.dependencies[pkgName] = version;
json.dependencies = sortObjectByKeys(json.dependencies);
}
}

// 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 (tree: Tree, _context: SchematicContext) => {
addPkg(tree, '@angular/animations', ANGULAR_CORE_VERSION);
addPkg(tree, '@alyle/ui', AUI_VERSION);
addPkg(tree, 'chroma-js', CHROMA_JS_VERSION);
if (_options.gestures) {
addPkg(tree, 'hammerjs', HAMMERJS_VERSION);
addHammerJsToMain(_options);
}
return chain([setUpAppModule(_options)]);
};
}
38 changes: 38 additions & 0 deletions src/lib/schematics/ng-add/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/schema",
"id": "alyle-ui-ng-add",
"title": "Alyle UI ng-add schematic",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "Name of the project.",
"$default": {
"$source": "projectName"
}
},
"gestures": {
"type": "boolean",
"default": true,
"description": "Whether gesture support should be set up.",
"x-prompt": "Set up HammerJS for gesture recognition?"
},
"themes": {
"type": "array",
"x-prompt": {
"message": "Select the themes that will be added to AppModule",
"type": "list",
"multiselect": true,
"items": [
{
"value": "minima-light", "label": "Minima Light"
},
{
"value": "minima-dark", "label": "Minima Dark"
}
]
}
}
},
"required": []
}
5 changes: 5 additions & 0 deletions src/lib/schematics/ng-add/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface Schema {
project: string;
gestures: boolean;
themes: string[];
}
170 changes: 170 additions & 0 deletions src/lib/schematics/ng-add/set-up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import * as inquirer from 'inquirer';
import * as semver from 'semver';
import { strings } from '@angular-devkit/core';
import * as ts from '@schematics/angular/node_modules/typescript/lib/typescript';
import { addSymbolToNgModuleMetadata, insertImport, isImported } from '@schematics/angular/utility/ast-utils';
import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils';
import { getProjectTargets, targetBuildNotFoundError } from '@schematics/angular/utility/project-targets';
import { InsertChange } from '@schematics/angular/utility/change';
import { join } from 'path';
import { Observable } from 'rxjs';
import {
Rule,
SchematicContext,
SchematicsException,
Tree
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

checkVersionAngularCLI();

function checkVersionAngularCLI() {
const version = require(join(process.cwd(), 'package.json')).devDependencies['@angular/cli'];
if (!semver.satisfies(semver.coerce(version)!, '>=7.2.0-rc.0 || 7.2.x')) {
throw new SchematicsException(`Alyle UI Schematics require "@angular/cli@>=7.2.0-rc.0 || 7.2.x"`);
}
}

function updateAppModule(host: Tree, _context: SchematicContext, options: any, themeName: string, themes: string[]) {
_context.logger.debug('Updating appmodule');
// find app module
const projectTargets = getProjectTargets(host, options.project);
if (!projectTargets.build) {
throw targetBuildNotFoundError();
}

const mainPath = projectTargets.build.options.main;
const modulePath = getAppModulePath(host, mainPath);
_context.logger.debug(`module path: ${modulePath}`);

// add import animations
let moduleSource = getTsSourceFile(host, modulePath);
let importModule = 'BrowserAnimationsModule';
let importPath = '@angular/platform-browser/animations';
if (!isImported(moduleSource, importModule, importPath)) {
const change = insertImport
(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd);
host.commitUpdate(recorder);
}
}

// register animations in app module
moduleSource = getTsSourceFile(host, modulePath);
let metadataChanges = addSymbolToNgModuleMetadata(
moduleSource, modulePath, 'imports', importModule);
if (metadataChanges) {
const recorder = host.beginUpdate(modulePath);
metadataChanges.forEach((change: InsertChange) => {
recorder.insertRight(change.pos, change.toAdd);
});
host.commitUpdate(recorder);
}

// add import theme
['LyThemeModule', 'LY_THEME'].forEach((_import) => {
moduleSource = getTsSourceFile(host, modulePath);
importModule = _import;
importPath = '@alyle/ui';
if (!isImported(moduleSource, importModule, importPath)) {
const change = insertImport
(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd);
host.commitUpdate(recorder);
}
}
});

// register theme in app module
const importText = `LyThemeModule.setTheme('${themeName}')`;
moduleSource = getTsSourceFile(host, modulePath);
metadataChanges = addSymbolToNgModuleMetadata(
moduleSource, modulePath, 'imports', importText);
if (!moduleSource.text.includes('LyThemeModule.setTheme') && metadataChanges) {
const recorder = host.beginUpdate(modulePath);
metadataChanges.forEach((change: InsertChange) => {
recorder.insertRight(change.pos, change.toAdd);
});
host.commitUpdate(recorder);
}

themes.forEach(_themeName => {
const [themePath] = _themeName.split('-');
// register providers
moduleSource = getTsSourceFile(host, modulePath);
const simbolName = `{ provide: LY_THEME, useClass: ${strings.classify(_themeName)}, multi: true }`;
metadataChanges = addSymbolToNgModuleMetadata(
moduleSource, modulePath, 'providers', simbolName);
if (metadataChanges) {
const recorder = host.beginUpdate(modulePath);
metadataChanges.forEach((change: InsertChange) => {
recorder.insertRight(change.pos, change.toAdd);
});
host.commitUpdate(recorder);
}

// add import themes
moduleSource = getTsSourceFile(host, modulePath);
importModule = strings.classify(_themeName);
importPath = `@alyle/ui/themes/${themePath}`;
if (!isImported(moduleSource, importModule, importPath)) {
const change = insertImport
(moduleSource, modulePath, importModule, importPath);
if (change) {
const recorder = host.beginUpdate(modulePath);
recorder.insertLeft((change as InsertChange).pos, (change as InsertChange).toAdd);
host.commitUpdate(recorder);
}
}
});
}

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;
}


// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function setUpAppModule(_options: Schema): Rule {
return (host: Tree, _context: SchematicContext) => {
return new Observable((_) => {
if (_options.themes.length > 1) {
inquirer
.prompt([
{
type: 'list',
name: 'selectedTheme',
message: 'Set Theme',
choices: _options.themes
},
])
.then(({selectedTheme}: { selectedTheme: string }) => {
const themes = _options.themes;
updateAppModule(host, _context, _options, selectedTheme, themes);
_.next(host);
_.complete();
_context.addTask(new NodePackageInstallTask());
});
} else {
const selectedTheme = [...(_options.themes as string[]), 'minima-light'][0];
updateAppModule(host, _context, _options, selectedTheme, [selectedTheme]);
_.next(host);
_.complete();
_context.addTask(new NodePackageInstallTask());
}
});
};
}
16 changes: 16 additions & 0 deletions src/lib/schematics/utils/get-project-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {WorkspaceProject} from '@angular-devkit/core/src/workspace';
import {SchematicsException} from '@angular-devkit/schematics';

/** Resolves the architect options for the build target of the given project. */
export function getProjectTargetOptions(project: WorkspaceProject, buildTarget: string) {
const targets = project.targets || project.architect;
if (targets &&
targets[buildTarget] &&
targets[buildTarget].options) {

return targets[buildTarget].options;
}

throw new SchematicsException(
`Cannot determine project target configuration for: ${buildTarget}.`);
}
19 changes: 19 additions & 0 deletions src/lib/schematics/utils/get-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { WorkspaceSchema, WorkspaceProject } from '@angular-devkit/core/src/workspace';
import { SchematicsException } from '@angular-devkit/schematics';

/**
* Finds the specified project configuration in the workspace. Throws an error if the project
* couldn't be found.
*/
export function getProjectFromWorkspace(
workspace: WorkspaceSchema,
projectName?: string): WorkspaceProject {

const project = workspace.projects[projectName || workspace.defaultProject!];

if (!project) {
throw new SchematicsException(`Could not find project in workspace: ${projectName}`);
}

return project;
}

0 comments on commit 340e8ef

Please sign in to comment.