diff --git a/package.json b/package.json index 8069de5..d93d8dd 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,15 @@ "test": "ng test sunbird-epub-player --code-coverage --watch=false", "lint": "ng lint", "e2e": "ng e2e", - "build-lib": "ng build sunbird-epub-player && node assets-copy.js", + "build-lib": "ng build sunbird-epub-player && node assets-copy.js && npm run schematics:build", "link-player": "cd dist/sunbird-epub-player && npm link && cd ../.. && npm link @project-sunbird/sunbird-epub-player-v9", "build-lib-link": "npm run build-lib && npm run link-player", "lib-test": "ng test sunbird-epub-player", "mybuild": "npm run build", "serve": "node assets-copy.js && npm link ./dist/sunbird-epub-player && ng serve", - "build-web-component": "npm run build-lib-link && ng build epub-player-wc --output-hashing none && node ./build-wc.js" + "build-web-component": "npm run build-lib-link && ng build epub-player-wc --output-hashing none && node ./build-wc.js", + "schematics:build": "./node_modules/.bin/tsc -p tsconfig.schematics.json", + "postschematics:build": "./node_modules/.bin/copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ./dist/sunbird-epub-player/" }, "private": true, "dependencies": { @@ -49,6 +51,7 @@ "@types/node": "^8.9.5", "codelyzer": "^5.0.0", "concat": "^1.0.3", + "copyfiles": "^2.4.1", "fs-extra": "^10.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", diff --git a/projects/sunbird-epub-player/package.json b/projects/sunbird-epub-player/package.json index a5e5e11..bf628a0 100644 --- a/projects/sunbird-epub-player/package.json +++ b/projects/sunbird-epub-player/package.json @@ -20,5 +20,9 @@ "sunbird epub player", "project-sunbird" ], - "license": "MIT" + "license": "MIT", + "schematics": "./schematics/collection.json", + "ng-add": { + "save": "devDependencies" + } } diff --git a/schematics/collection.json b/schematics/collection.json new file mode 100644 index 0000000..490803c --- /dev/null +++ b/schematics/collection.json @@ -0,0 +1,18 @@ +{ + "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Add @project-sunbird/sunbird-epub-player-v9 to the project.", + "factory": "./ng-add/index", + "schema": "./ng-add/schema.json", + "aliases": ["install"] + }, + "ng-add-setup-project": { + "private": true, + "description": "Sets up the specified project after the ng-add dependencies have been installed.", + "factory": "./ng-add/setup-project", + "schema": "./ng-add/schema.json" + } + } + } + \ No newline at end of file diff --git a/schematics/ng-add/index.ts b/schematics/ng-add/index.ts new file mode 100644 index 0000000..c5de05a --- /dev/null +++ b/schematics/ng-add/index.ts @@ -0,0 +1,53 @@ +import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; +import { + NodePackageInstallTask, + RunSchematicTask, +} from '@angular-devkit/schematics/tasks'; + +import {getWorkspace} from '@schematics/angular/utility/workspace'; + +import {Schema} from './schema'; +import * as messages from './messages'; +import {addPackageToPackageJson} from '../utils/package-config'; + +interface VersionOptions { + [key: string]: string; +} + +const VERSIONS: VersionOptions = { + // automatically filled from package.json during the build + '@project-sunbird/sb-styles': '0.0.7', + '@project-sunbird/client-services': '^3.4.8', + epubjs: '0.3.88', +}; + +/** + * This is executed when `ng add @project-sunbird/sunbird-epub-player-v9` is run. + * It installs all dependencies in the 'package.json' and runs 'ng-add-setup-project' schematic. + */ +export default function ngAdd(options: Schema): Rule { + return async (tree: Tree, context: SchematicContext) => { + + // Checking that project exists + const {project} = options; + if (project) { + const workspace = await getWorkspace(tree); + const projectWorkspace = workspace.projects.get(project); + + if (!projectWorkspace) { + throw new SchematicsException(messages.noProject(project)); + } + } + + // Installing dependencies + for (const key in VERSIONS) { + if (VERSIONS.hasOwnProperty(key)) { + addPackageToPackageJson(tree, key, VERSIONS[key]); + } + } + + context.addTask(new RunSchematicTask('ng-add-setup-project', options), [ + context.addTask(new NodePackageInstallTask()), + ]); + }; +} diff --git a/schematics/ng-add/messages.ts b/schematics/ng-add/messages.ts new file mode 100644 index 0000000..65597bb --- /dev/null +++ b/schematics/ng-add/messages.ts @@ -0,0 +1,11 @@ +export function noProject(project: string) { + return `Unable to find project '${project}' in the workspace`; + } + +export function noMainFile(projectName: string) { + return `Unable to find 'build.options.main' file path for project "${projectName}"`; + } + +export function noModuleFile(moduleFilePath: string) { + return `File '${moduleFilePath}' does not exist.`; + } diff --git a/schematics/ng-add/schema.json b/schematics/ng-add/schema.json new file mode 100644 index 0000000..91f4e67 --- /dev/null +++ b/schematics/ng-add/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "sunbird-epub-player-ng-add", + "title": "Sunbird Epub Player ng-add schematic", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Name of the project.", + "$default": { + "$source": "projectName" + } + } + }, + "required": [] + } \ No newline at end of file diff --git a/schematics/ng-add/schema.ts b/schematics/ng-add/schema.ts new file mode 100644 index 0000000..a349f81 --- /dev/null +++ b/schematics/ng-add/schema.ts @@ -0,0 +1,6 @@ +export interface Schema { + /** + * Name of the project where sunbird-epub-player library should be installed + */ + project?: string; + } diff --git a/schematics/ng-add/setup-project.ts b/schematics/ng-add/setup-project.ts new file mode 100644 index 0000000..1231753 --- /dev/null +++ b/schematics/ng-add/setup-project.ts @@ -0,0 +1,14 @@ +import {chain, Rule} from '@angular-devkit/schematics'; +import {Schema} from './schema'; +import {addPlayerModuleToAppModule} from './steps/add-player-module'; +import { addPlayerStyles } from './steps/add-player-style'; +/** + * Sets up a project with all required to run sunbird pdf player. + * This is run after 'package.json' was patched and all dependencies installed + */ +export default function ngAddSetupProject(options: Schema): Rule { + return chain([ + addPlayerModuleToAppModule(options), + addPlayerStyles(options), + ]); +} diff --git a/schematics/ng-add/steps/add-player-module.ts b/schematics/ng-add/steps/add-player-module.ts new file mode 100644 index 0000000..b9a7a45 --- /dev/null +++ b/schematics/ng-add/steps/add-player-module.ts @@ -0,0 +1,75 @@ +import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics'; +import {getAppModulePath} from '@schematics/angular/utility/ng-ast-utils'; +import {addImportToModule} from '@schematics/angular/utility/ast-utils'; +import {InsertChange} from '@schematics/angular/utility/change'; +import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import {Schema} from '../schema'; +import {getWorkspace} from '@schematics/angular/utility/workspace'; +import * as messages from '../messages'; +import { getProjectTargetOptions } from '../../utils/project'; + +const MODULE_NAME = 'SunbirdEpubPlayerModule'; +const PACKAGE_NAME = '@project-sunbird/sunbird-epub-player-v9'; + +/** + * Patches main application module by adding 'SunbirdEpubPlayerModule' import. + * + * Relevant 'angular.json' structure is: + * + * { + * "projects" : { + * "projectName": { + * "architect": { + * "build": { + * "options": { + * "main": "src/main.ts" + * } + * } + * } + * } + * }, + * "defaultProject": "projectName" + * } + * + */ +export function addPlayerModuleToAppModule(options: Schema): Rule { + return async (host: Tree) => { + const workspace = await getWorkspace(host); + const projectName = options.project || (workspace.extensions.defaultProject as string); + + // 1. getting project by name + const project: any = workspace.projects.get(projectName); + if (!project) { + throw new SchematicsException(messages.noProject(projectName)); + } + + // 2. getting main file for project + const projectBuildOptions = getProjectTargetOptions(project, 'build'); + const mainFilePath = projectBuildOptions.main as string; + if (!mainFilePath || !host.read(mainFilePath)) { + throw new SchematicsException(messages.noMainFile(projectName)); + } + + // 3. getting main app module file + const appModuleFilePath = getAppModulePath(host, mainFilePath); + const appModuleFileText = host.read(appModuleFilePath); + if (appModuleFileText === null) { + throw new SchematicsException(messages.noModuleFile(appModuleFilePath)); + } + + // 4. adding `NgbModule` to the app module + const appModuleSource = + ts.createSourceFile(appModuleFilePath, appModuleFileText.toString('utf-8'), ts.ScriptTarget.Latest, true); + + const changes = + addImportToModule(appModuleSource, appModuleFilePath, MODULE_NAME, PACKAGE_NAME); + + const recorder = host.beginUpdate(appModuleFilePath); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(recorder); + }; +} diff --git a/schematics/ng-add/steps/add-player-style.ts b/schematics/ng-add/steps/add-player-style.ts new file mode 100644 index 0000000..6c91591 --- /dev/null +++ b/schematics/ng-add/steps/add-player-style.ts @@ -0,0 +1,94 @@ +import { Rule, Tree, SchematicsException } from '@angular-devkit/schematics'; +import { Schema } from '../schema'; +import * as messages from '../messages'; +import { getProjectTargetOptions } from '../../utils/project'; +import { getWorkspace, updateWorkspace } from '@schematics/angular/utility/workspace'; +import { workspaces, JsonArray } from '@angular-devkit/core'; + +// Default styles, assets and script + +const SB_STYLES = [ + 'node_modules/@project-sunbird/sb-styles/assets/_styles.scss' +]; +const SB_ASSETS = [{ + glob: '**/*.*', + input: './node_modules/@project-sunbird/sunbird-epub-player-v9/lib/assets/', + output: '/assets/' +}]; +const SB_SCRIPTS = [ + 'node_modules/epubjs/dist/epub.js' +]; + + +/** + * we're simply adding styles to the 'angular.json' + */ +export function addPlayerStyles(options: Schema): Rule { + return async (host: Tree) => { + const workspace: any = await getWorkspace(host); + + const projectName = options.project || (workspace.extensions.defaultProject as string); + const project = workspace.projects.get(projectName); + if (!project) { + throw new SchematicsException(messages.noProject(projectName)); + } + // just patching 'angular.json' + return addPlayerToAngularJson(workspace, project); + }; +} + +/** + * Patches 'angular.json' to add styles + */ +function addPlayerToAngularJson( + workspace: any, + project: workspaces.ProjectDefinition +): Rule { + const targetOptions = getProjectTargetOptions(project, 'build'); + addStyleToTarget(targetOptions, SB_STYLES); + addAssetsToTarget(targetOptions, SB_ASSETS); + addScriptToTarget(targetOptions, SB_SCRIPTS); + return updateWorkspace(workspace); +} + +function addStyleToTarget(targetOptions: any, assetPaths: Array) { + const styles = (targetOptions.styles as JsonArray | undefined); + if (!styles) { + targetOptions.styles = assetPaths; + } else { + const existingStyles: any = styles.map((s: any) => typeof s === 'string' ? s : s.input); + assetPaths.forEach((style: any) => { + if (!existingStyles.includes(typeof style === 'string' ? style : style.input)) { + styles.unshift(style); + } + }); + } +} + +function addAssetsToTarget(targetOptions: any, assetPaths: Array) { + const assets = (targetOptions.assets as JsonArray | undefined); + if (!assets) { + targetOptions.assets = assetPaths; + } else { + const existingAssets: any = assets.map((s: any) => typeof s === 'string' ? s : s.input); + assetPaths.forEach((asset: any) => { + if (!existingAssets.includes(typeof asset === 'string' ? asset : asset.input)) { + assets.unshift(asset); + } + }); + } +} + +function addScriptToTarget(targetOptions: any, assetPaths: Array) { + const scripts = (targetOptions.scripts as JsonArray | undefined); + if (!scripts) { + targetOptions.scripts = assetPaths; + } else { + const existingScripts: any = scripts.map((s: any) => typeof s === 'string' ? s : s.input); + assetPaths.forEach((script: any) => { + if (!existingScripts.includes(typeof script === 'string' ? script : script.input)) { + scripts.unshift(script); + } + }); + } +} diff --git a/schematics/utils/package-config.ts b/schematics/utils/package-config.ts new file mode 100644 index 0000000..e5d02be --- /dev/null +++ b/schematics/utils/package-config.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + + import {Tree} from '@angular-devkit/schematics'; + + interface PackageJson { + dependencies: Record; + } + + /** + * Sorts the keys of the given object. + * @returns A new object instance with sorted keys + */ + function sortObjectByKeys(obj: Record) { + return Object.keys(obj) + .sort() + .reduce((result, key) => { + result[key] = obj[key]; + return result; + }, {} as Record); + } + + /** Adds a package to the package.json in the given host tree. */ + export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree { + if (host.exists('package.json')) { + // tslint:disable-next-line:no-non-null-assertion + const sourceText = host.read('package.json')!.toString('utf-8'); + const json = JSON.parse(sourceText) as PackageJson; + + if (!json.dependencies) { + json.dependencies = {}; + } + + if (!json.dependencies[pkg]) { + json.dependencies[pkg] = version; + json.dependencies = sortObjectByKeys(json.dependencies); + } + + host.overwrite('package.json', JSON.stringify(json, null, 2)); + } + + return host; + } + + /** Gets the version of the specified package by looking at the package.json in the given tree. */ + export function getPackageVersionFromPackageJson(tree: Tree, name: string): string | null { + if (!tree.exists('package.json')) { + return null; + } + + // tslint:disable-next-line:no-non-null-assertion + const packageJson = JSON.parse(tree.read('package.json')!.toString('utf8')) as PackageJson; + + if (packageJson.dependencies && packageJson.dependencies[name]) { + return packageJson.dependencies[name]; + } + + return null; + } diff --git a/schematics/utils/project.ts b/schematics/utils/project.ts new file mode 100644 index 0000000..1695a30 --- /dev/null +++ b/schematics/utils/project.ts @@ -0,0 +1,11 @@ +import {workspaces} from '@angular-devkit/core'; +import {SchematicsException} from '@angular-devkit/schematics'; + +export function getProjectTargetOptions(project: workspaces.ProjectDefinition, buildTarget: string) { + const buildTargetObject = project.targets.get(buildTarget); + if (buildTargetObject && buildTargetObject.options) { + return buildTargetObject.options; + } + + throw new SchematicsException(`Cannot determine project target configuration for: ${buildTarget}.`); +} diff --git a/tsconfig.schematics.json b/tsconfig.schematics.json new file mode 100644 index 0000000..6c8dccd --- /dev/null +++ b/tsconfig.schematics.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "baseUrl": "./tsconfig.schematics.json", + "lib": [ + "es2018", + "dom" + ], + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "rootDir": "schematics", + "outDir": "./dist/sunbird-epub-player/schematics", + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strictNullChecks": true, + "target": "es6", + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "schematics/**/*" + ], + "exclude": [ + "schematics/*/files/**/*" + ] + } \ No newline at end of file