Skip to content

Commit 2dc2f47

Browse files
billyjovmattlewis92
authored andcommitted
feat(schematics): support ng add schematics
Closes #888
1 parent 00deb58 commit 2dc2f47

15 files changed

+601
-18
lines changed

package-lock.json

+61-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
"build": "run-s build:copy-package-json build:lib build:date-adapters build:styles build:schematics build:copyfiles",
1515
"test:single": "cross-env TZ=UTC ng test angular-calendar --watch=false --code-coverage",
1616
"test:watch": "cross-env TZ=UTC ng test angular-calendar",
17-
"test:schematics": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register projects/angular-calendar/schematics/**/*.spec.ts",
17+
"test:schematics": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} mocha --require ts-node/register projects/angular-calendar/schematics/**/*.spec.ts",
1818
"test": "run-s lint test:single test:schematics build build:clean",
1919
"lint:styles": "stylelint \"{projects,src}/**/*.scss\" --fix",
2020
"lint:ts": "ng lint",
2121
"lint": "run-s lint:ts lint:styles",
2222
"commit": "git-cz",
2323
"codecov": "cat coverage/lcov.info | codecov",
2424
"prerelease": "npm test",
25+
"pretest:schematics": "npm run build:copy-package-json",
2526
"release:git-add": "git add package.json package-lock.json",
2627
"release:git-commit": "git commit -m 'chore: bump version number'",
2728
"release:git-changelog": "standard-version --first-release",
@@ -84,6 +85,7 @@
8485
"@commitlint/prompt": "^8.2.0",
8586
"@compodoc/compodoc": "^1.1.10",
8687
"@ng-bootstrap/ng-bootstrap": "^5.1.1",
88+
"@schematics/angular": "^8.3.8",
8789
"@stackblitz/sdk": "^1.3.0",
8890
"@types/chai": "^4.2.3",
8991
"@types/mocha": "^5.2.7",
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,103 @@
1-
import { Tree } from '@angular-devkit/schematics';
2-
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
1+
import {
2+
SchematicTestRunner,
3+
UnitTestTree
4+
} from '@angular-devkit/schematics/testing';
5+
import { getWorkspace } from '@schematics/angular/utility/config';
6+
37
import * as path from 'path';
48
import { expect } from 'chai';
59

10+
import { createTestApp } from '../testing/workspace';
11+
import { Schema } from './schema';
12+
import { angularCalendarVersion, momentVersion } from './version-names';
13+
import { getProjectFromWorkspace, getProjectTargetOptions } from '../utils';
14+
615
const collectionPath = path.join(__dirname, '../collection.json');
716

17+
export interface PackageJson {
18+
dependencies: DependencyMap;
19+
devDependencies: DependencyMap;
20+
}
21+
22+
export interface DependencyMap {
23+
[dependencyName: string]: string;
24+
}
25+
26+
const defaultAngularCalendarStylePath =
27+
'node_modules/angular-calendar/css/angular-calendar.css';
28+
829
describe('angular-calendar schematics', () => {
9-
it('works', async () => {
10-
const runner = new SchematicTestRunner('schematics', collectionPath);
11-
const tree = await runner
12-
.runSchematicAsync('ng-add', {}, Tree.empty())
30+
const projectName = 'angular-calendar-app';
31+
const defaultOptions = {} as Schema;
32+
let tree: UnitTestTree;
33+
let appTree: UnitTestTree;
34+
let runner: SchematicTestRunner;
35+
let packageJsonPath: string;
36+
let packageJson: PackageJson;
37+
38+
beforeEach(async () => {
39+
appTree = await createTestApp({ name: projectName });
40+
runner = new SchematicTestRunner(
41+
'angular-calendar-schematics',
42+
collectionPath
43+
);
44+
packageJsonPath = '/package.json';
45+
});
46+
47+
it('should add angular-calendar to dependencies', async () => {
48+
const { name, version } = {
49+
name: 'angular-calendar',
50+
version: angularCalendarVersion
51+
};
52+
tree = await runner
53+
.runSchematicAsync('ng-add', defaultOptions, appTree)
54+
.toPromise();
55+
packageJson = JSON.parse(tree.readContent(packageJsonPath));
56+
expect(packageJson.dependencies[name]).equal(version);
57+
});
58+
59+
it('should add date adapter to dependencies based on option selected ', async () => {
60+
const { name, version } = { name: 'moment', version: momentVersion };
61+
defaultOptions.dateAdapter = 'moment';
62+
tree = await runner
63+
.runSchematicAsync('ng-add', defaultOptions, appTree)
1364
.toPromise();
65+
packageJson = JSON.parse(tree.readContent(packageJsonPath));
66+
expect(packageJson.dependencies[name]).equal(version);
67+
});
68+
69+
it('should schedule install dependencies task', async () => {
70+
await runner
71+
.runSchematicAsync('ng-add', defaultOptions, appTree)
72+
.toPromise();
73+
const tasks = runner.tasks;
74+
expect(tasks.length).to.equal(1);
75+
});
76+
77+
it('should import angular-calendar module to root module', async () => {
78+
const rootModulePath = `/projects/${projectName}/src/app/app.module.ts`;
79+
tree = await runner
80+
.runSchematicAsync('ng-add', defaultOptions, appTree)
81+
.toPromise();
82+
expect(tree.files).contain(rootModulePath);
83+
84+
const rootModule = tree.readContent(rootModulePath);
85+
86+
const calendarModuleImport = `import { CalendarModule } from 'angular-calendar'`;
87+
expect(rootModule).contain(calendarModuleImport);
88+
});
89+
90+
it('should add angular-calendar css to architect builder', async () => {
91+
tree = await runner
92+
.runSchematicAsync('ng-add', defaultOptions, appTree)
93+
.toPromise();
94+
95+
const workspace = getWorkspace(tree);
96+
const project = getProjectFromWorkspace(workspace);
97+
const styles = getProjectTargetOptions(project, 'build').styles;
98+
const stylesTest = getProjectTargetOptions(project, 'test').styles;
1499

15-
expect(tree.files).to.deep.equal([]);
100+
expect(styles[0]).to.contains(defaultAngularCalendarStylePath);
101+
expect(stylesTest[0]).to.contains(defaultAngularCalendarStylePath);
16102
});
17103
});
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,150 @@
1-
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
1+
import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript';
2+
import {
3+
Rule,
4+
SchematicContext,
5+
Tree,
6+
chain
7+
} from '@angular-devkit/schematics';
8+
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
9+
import { getWorkspace } from '@schematics/angular/utility/config';
10+
import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils';
11+
import { insertImport } from '@schematics/angular/utility/ast-utils';
12+
import { InsertChange } from '@schematics/angular/utility/change';
13+
import {
14+
addPackageJsonDependency,
15+
NodeDependency,
16+
NodeDependencyType
17+
} from '@schematics/angular/utility/dependencies';
18+
19+
import {
20+
addModuleImportToRootModule,
21+
addStyle,
22+
getSourceFile,
23+
getProjectMainFile,
24+
getProjectFromWorkspace
25+
} from '../utils';
26+
227
import { Schema } from './schema';
28+
import {
29+
dateFnsVersion,
30+
momentVersion,
31+
angularCalendarVersion
32+
} from './version-names';
333

434
export default function(options: Schema): Rule {
5-
return (tree: Tree, _context: SchematicContext) => {
6-
return tree;
35+
return chain([
36+
addPackageJsonDependencies(options),
37+
installPackageJsonDependencies(),
38+
addModuleToImports(options),
39+
addAngularCalendarStyle(options)
40+
]);
41+
}
42+
43+
function installPackageJsonDependencies(): Rule {
44+
return (host: Tree, context: SchematicContext) => {
45+
context.addTask(new NodePackageInstallTask());
46+
context.logger.log('info', `Installing angular calendar dependencies...`);
47+
48+
return host;
49+
};
50+
}
51+
52+
function addPackageJsonDependencies(options: Schema): Rule {
53+
return (host: Tree, context: SchematicContext) => {
54+
const dateAdapters: { [key: string]: string } = {
55+
moment: momentVersion,
56+
'date-fns': dateFnsVersion
57+
};
58+
59+
const angularCalendarDependency: NodeDependency = nodeDependencyFactory(
60+
'angular-calendar',
61+
angularCalendarVersion
62+
);
63+
const dateAdapterLibrary = options.dateAdapter;
64+
const dateAdapterLibraryDependency: NodeDependency = nodeDependencyFactory(
65+
dateAdapterLibrary,
66+
dateAdapters[dateAdapterLibrary]
67+
);
68+
69+
addPackageJsonDependency(host, angularCalendarDependency);
70+
context.logger.log(
71+
'info',
72+
`Added "${angularCalendarDependency.name}" into ${angularCalendarDependency.type}`
73+
);
74+
75+
addPackageJsonDependency(host, dateAdapterLibraryDependency);
76+
context.logger.log(
77+
'info',
78+
`Added "${dateAdapterLibraryDependency.name}" into ${dateAdapterLibraryDependency.type}`
79+
);
80+
81+
return host;
82+
};
83+
}
84+
85+
function nodeDependencyFactory(
86+
packageName: string,
87+
version: string
88+
): NodeDependency {
89+
return {
90+
type: NodeDependencyType.Default,
91+
name: packageName,
92+
version,
93+
overwrite: true
94+
};
95+
}
96+
97+
function addModuleToImports(options: Schema): Rule {
98+
return (host: Tree, context: SchematicContext) => {
99+
context.logger.log('info', `Add modules imports options...`);
100+
101+
const workspace = getWorkspace(host);
102+
const project = getProjectFromWorkspace(
103+
workspace,
104+
options.projectName
105+
? options.projectName
106+
: Object.keys(workspace['projects'])[0]
107+
);
108+
const mainPath = getProjectMainFile(project);
109+
const appModulePath = options.module
110+
? options.module
111+
: getAppModulePath(host, mainPath);
112+
const moduleSource = getSourceFile(host, appModulePath);
113+
const moduleName = `CalendarModule.forRoot({ provide: DateAdapter, useFactory: adapterFactory })`;
114+
const moduleCalendarSrc = 'angular-calendar';
115+
const PEER_DEPENDENCIES = ['DateAdapter', 'adapterFactory'];
116+
117+
addModuleImportToRootModule(host, moduleName, moduleCalendarSrc, project);
118+
119+
const peerDependencyChange1 = insertImport(
120+
moduleSource as ts.SourceFile,
121+
appModulePath,
122+
PEER_DEPENDENCIES[0],
123+
moduleCalendarSrc
124+
) as InsertChange;
125+
126+
const peerDependencyChange2 = insertImport(
127+
moduleSource as ts.SourceFile,
128+
appModulePath,
129+
PEER_DEPENDENCIES[1],
130+
`${moduleCalendarSrc}/date-adapters/${options.dateAdapter}`
131+
) as InsertChange;
132+
133+
const recorder = host.beginUpdate(appModulePath);
134+
135+
recorder.insertLeft(peerDependencyChange1.pos, peerDependencyChange1.toAdd);
136+
recorder.insertLeft(peerDependencyChange2.pos, peerDependencyChange2.toAdd);
137+
host.commitUpdate(recorder);
138+
139+
return host;
140+
};
141+
}
142+
143+
function addAngularCalendarStyle(options: Schema): Rule {
144+
return (host: Tree) => {
145+
const libStylePath =
146+
'node_modules/angular-calendar/css/angular-calendar.css';
147+
addStyle(host, libStylePath, options.projectName);
148+
return host;
7149
};
8150
}

projects/angular-calendar/schematics/ng-add/schema.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
"description": "Which date adapter to use",
99
"type": "string",
1010
"default": "date-fns",
11+
"enum": [
12+
"moment",
13+
"date-fns"
14+
],
1115
"x-prompt": {
1216
"message": "What date adapter would you like to use?",
1317
"type": "list",
@@ -20,7 +24,12 @@
2024
"module": {
2125
"type": "string",
2226
"description": "Where to add the module import",
23-
"x-prompt": "Please enter a path to the NgModule that will use the calendar"
27+
"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)"
28+
},
29+
"projectName": {
30+
"type": "string",
31+
"description": "Which project should the styles be added to",
32+
"x-prompt": "Please enter the name of the project that will use the calendar (optional, will use the default project if not specified)"
2433
}
2534
},
2635
"required": [],

0 commit comments

Comments
 (0)