Skip to content

Commit

Permalink
feat(schematics): support ng add schematics
Browse files Browse the repository at this point in the history
Closes #888
  • Loading branch information
billyjov authored and mattlewis92 committed Nov 4, 2019
1 parent 00deb58 commit 2dc2f47
Show file tree
Hide file tree
Showing 15 changed files with 601 additions and 18 deletions.
66 changes: 61 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
"build": "run-s build:copy-package-json build:lib build:date-adapters build:styles build:schematics build:copyfiles",
"test:single": "cross-env TZ=UTC ng test angular-calendar --watch=false --code-coverage",
"test:watch": "cross-env TZ=UTC ng test angular-calendar",
"test:schematics": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register projects/angular-calendar/schematics/**/*.spec.ts",
"test:schematics": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha --require ts-node/register projects/angular-calendar/schematics/**/*.spec.ts",
"test": "run-s lint test:single test:schematics build build:clean",
"lint:styles": "stylelint \"{projects,src}/**/*.scss\" --fix",
"lint:ts": "ng lint",
"lint": "run-s lint:ts lint:styles",
"commit": "git-cz",
"codecov": "cat coverage/lcov.info | codecov",
"prerelease": "npm test",
"pretest:schematics": "npm run build:copy-package-json",
"release:git-add": "git add package.json package-lock.json",
"release:git-commit": "git commit -m 'chore: bump version number'",
"release:git-changelog": "standard-version --first-release",
Expand Down Expand Up @@ -84,6 +85,7 @@
"@commitlint/prompt": "^8.2.0",
"@compodoc/compodoc": "^1.1.10",
"@ng-bootstrap/ng-bootstrap": "^5.1.1",
"@schematics/angular": "^8.3.8",
"@stackblitz/sdk": "^1.3.0",
"@types/chai": "^4.2.3",
"@types/mocha": "^5.2.7",
Expand Down
100 changes: 93 additions & 7 deletions projects/angular-calendar/schematics/ng-add/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,103 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import {
SchematicTestRunner,
UnitTestTree
} from '@angular-devkit/schematics/testing';
import { getWorkspace } from '@schematics/angular/utility/config';

import * as path from 'path';
import { expect } from 'chai';

import { createTestApp } from '../testing/workspace';
import { Schema } from './schema';
import { angularCalendarVersion, momentVersion } from './version-names';
import { getProjectFromWorkspace, getProjectTargetOptions } from '../utils';

const collectionPath = path.join(__dirname, '../collection.json');

export interface PackageJson {
dependencies: DependencyMap;
devDependencies: DependencyMap;
}

export interface DependencyMap {
[dependencyName: string]: string;
}

const defaultAngularCalendarStylePath =
'node_modules/angular-calendar/css/angular-calendar.css';

describe('angular-calendar schematics', () => {
it('works', async () => {
const runner = new SchematicTestRunner('schematics', collectionPath);
const tree = await runner
.runSchematicAsync('ng-add', {}, Tree.empty())
const projectName = 'angular-calendar-app';
const defaultOptions = {} as Schema;
let tree: UnitTestTree;
let appTree: UnitTestTree;
let runner: SchematicTestRunner;
let packageJsonPath: string;
let packageJson: PackageJson;

beforeEach(async () => {
appTree = await createTestApp({ name: projectName });
runner = new SchematicTestRunner(
'angular-calendar-schematics',
collectionPath
);
packageJsonPath = '/package.json';
});

it('should add angular-calendar to dependencies', async () => {
const { name, version } = {
name: 'angular-calendar',
version: angularCalendarVersion
};
tree = await runner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
packageJson = JSON.parse(tree.readContent(packageJsonPath));
expect(packageJson.dependencies[name]).equal(version);
});

it('should add date adapter to dependencies based on option selected ', async () => {
const { name, version } = { name: 'moment', version: momentVersion };
defaultOptions.dateAdapter = 'moment';
tree = await runner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
packageJson = JSON.parse(tree.readContent(packageJsonPath));
expect(packageJson.dependencies[name]).equal(version);
});

it('should schedule install dependencies task', async () => {
await runner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
const tasks = runner.tasks;
expect(tasks.length).to.equal(1);
});

it('should import angular-calendar module to root module', async () => {
const rootModulePath = `/projects/${projectName}/src/app/app.module.ts`;
tree = await runner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();
expect(tree.files).contain(rootModulePath);

const rootModule = tree.readContent(rootModulePath);

const calendarModuleImport = `import { CalendarModule } from 'angular-calendar'`;
expect(rootModule).contain(calendarModuleImport);
});

it('should add angular-calendar css to architect builder', async () => {
tree = await runner
.runSchematicAsync('ng-add', defaultOptions, appTree)
.toPromise();

const workspace = getWorkspace(tree);
const project = getProjectFromWorkspace(workspace);
const styles = getProjectTargetOptions(project, 'build').styles;
const stylesTest = getProjectTargetOptions(project, 'test').styles;

expect(tree.files).to.deep.equal([]);
expect(styles[0]).to.contains(defaultAngularCalendarStylePath);
expect(stylesTest[0]).to.contains(defaultAngularCalendarStylePath);
});
});
148 changes: 145 additions & 3 deletions projects/angular-calendar/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,150 @@
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript';
import {
Rule,
SchematicContext,
Tree,
chain
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { getWorkspace } from '@schematics/angular/utility/config';
import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils';
import { insertImport } from '@schematics/angular/utility/ast-utils';
import { InsertChange } from '@schematics/angular/utility/change';
import {
addPackageJsonDependency,
NodeDependency,
NodeDependencyType
} from '@schematics/angular/utility/dependencies';

import {
addModuleImportToRootModule,
addStyle,
getSourceFile,
getProjectMainFile,
getProjectFromWorkspace
} from '../utils';

import { Schema } from './schema';
import {
dateFnsVersion,
momentVersion,
angularCalendarVersion
} from './version-names';

export default function(options: Schema): Rule {
return (tree: Tree, _context: SchematicContext) => {
return tree;
return chain([
addPackageJsonDependencies(options),
installPackageJsonDependencies(),
addModuleToImports(options),
addAngularCalendarStyle(options)
]);
}

function installPackageJsonDependencies(): Rule {
return (host: Tree, context: SchematicContext) => {
context.addTask(new NodePackageInstallTask());
context.logger.log('info', `Installing angular calendar dependencies...`);

return host;
};
}

function addPackageJsonDependencies(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const dateAdapters: { [key: string]: string } = {
moment: momentVersion,
'date-fns': dateFnsVersion
};

const angularCalendarDependency: NodeDependency = nodeDependencyFactory(
'angular-calendar',
angularCalendarVersion
);
const dateAdapterLibrary = options.dateAdapter;
const dateAdapterLibraryDependency: NodeDependency = nodeDependencyFactory(
dateAdapterLibrary,
dateAdapters[dateAdapterLibrary]
);

addPackageJsonDependency(host, angularCalendarDependency);
context.logger.log(
'info',
`Added "${angularCalendarDependency.name}" into ${angularCalendarDependency.type}`
);

addPackageJsonDependency(host, dateAdapterLibraryDependency);
context.logger.log(
'info',
`Added "${dateAdapterLibraryDependency.name}" into ${dateAdapterLibraryDependency.type}`
);

return host;
};
}

function nodeDependencyFactory(
packageName: string,
version: string
): NodeDependency {
return {
type: NodeDependencyType.Default,
name: packageName,
version,
overwrite: true
};
}

function addModuleToImports(options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
context.logger.log('info', `Add modules imports options...`);

const workspace = getWorkspace(host);
const project = getProjectFromWorkspace(
workspace,
options.projectName
? options.projectName
: Object.keys(workspace['projects'])[0]
);
const mainPath = getProjectMainFile(project);
const appModulePath = options.module
? options.module
: getAppModulePath(host, mainPath);
const moduleSource = getSourceFile(host, appModulePath);
const moduleName = `CalendarModule.forRoot({ provide: DateAdapter, useFactory: adapterFactory })`;
const moduleCalendarSrc = 'angular-calendar';
const PEER_DEPENDENCIES = ['DateAdapter', 'adapterFactory'];

addModuleImportToRootModule(host, moduleName, moduleCalendarSrc, project);

const peerDependencyChange1 = insertImport(
moduleSource as ts.SourceFile,
appModulePath,
PEER_DEPENDENCIES[0],
moduleCalendarSrc
) as InsertChange;

const peerDependencyChange2 = insertImport(
moduleSource as ts.SourceFile,
appModulePath,
PEER_DEPENDENCIES[1],
`${moduleCalendarSrc}/date-adapters/${options.dateAdapter}`
) as InsertChange;

const recorder = host.beginUpdate(appModulePath);

recorder.insertLeft(peerDependencyChange1.pos, peerDependencyChange1.toAdd);
recorder.insertLeft(peerDependencyChange2.pos, peerDependencyChange2.toAdd);
host.commitUpdate(recorder);

return host;
};
}

function addAngularCalendarStyle(options: Schema): Rule {
return (host: Tree) => {
const libStylePath =
'node_modules/angular-calendar/css/angular-calendar.css';
addStyle(host, libStylePath, options.projectName);
return host;
};
}
11 changes: 10 additions & 1 deletion projects/angular-calendar/schematics/ng-add/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"description": "Which date adapter to use",
"type": "string",
"default": "date-fns",
"enum": [
"moment",
"date-fns"
],
"x-prompt": {
"message": "What date adapter would you like to use?",
"type": "list",
Expand All @@ -20,7 +24,12 @@
"module": {
"type": "string",
"description": "Where to add the module import",
"x-prompt": "Please enter a path to the NgModule that will use the calendar"
"x-prompt": "Please enter a path to the NgModule that will use the calendar (relative to the root project directory, for example src/app/app.module.ts)"
},
"projectName": {
"type": "string",
"description": "Which project should the styles be added to",
"x-prompt": "Please enter the name of the project that will use the calendar (optional, will use the default project if not specified)"
}
},
"required": [],
Expand Down
Loading

0 comments on commit 2dc2f47

Please sign in to comment.