import { execSync } from 'child_process'; import * as fs from 'fs'; import { readFileSync, statSync, writeFileSync } from 'fs'; import * as path from 'path'; import { copySync, removeSync } from 'fs-extra'; import { FileSystemEngineHost, NodeModulesEngineHost, validateOptionsWithSchema } from '@angular-devkit/schematics/tools'; import { BuiltinTaskExecutor } from '@angular-devkit/schematics/tasks/node'; import { CollectionDescription, EngineHost, RuleFactory, Schematic, SchematicDescription, SchematicEngine, Tree, DryRunSink, TypedSchematicContext, HostSink, HostTree } from '@angular-devkit/schematics'; import { of } from 'rxjs'; import { concat, concatMap, ignoreElements, map } from 'rxjs/operators'; import { Url } from 'url'; import * as yargsParser from 'yargs-parser'; import { CoreSchemaRegistry } from '@angular-devkit/core/src/json/schema'; import { standardFormats } from '@angular-devkit/schematics/src/formats'; import * as appRoot from 'app-root-path'; import { virtualFs, normalize } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; const rootDirectory = appRoot.path; export function workspaceSchematic(args: string[]) { const outDir = compileTools(); const schematicName = args[0]; const { schematic, host, engine } = prepareExecutionContext( outDir, schematicName ); executeSchematic(schematicName, parseOptions(args), schematic, host, engine); } // compile tools function compileTools() { const toolsOutDir = getToolsOutDir(); removeSync(toolsOutDir); compileToolsDir(toolsOutDir); const schematicsOutDir = path.join(toolsOutDir, 'schematics'); const collectionData = constructCollection(); saveCollection(schematicsOutDir, collectionData); return schematicsOutDir; } function getToolsOutDir() { return path.resolve( rootDirectory, 'tools', JSON.parse( readFileSync( path.join(rootDirectory, 'tools', 'tsconfig.tools.json'), 'UTF-8' ) ).compilerOptions.outDir ); } function compileToolsDir(outDir: string) { copySync(path.join(rootDirectory, 'tools'), outDir); try { execSync('tsc -p tools/tsconfig.tools.json', { stdio: 'inherit', cwd: rootDirectory }); } catch (e) { process.exit(1); } } function constructCollection() { const schematics = {}; fs.readdirSync(schematicsDir()).forEach(c => { const childDir = path.join(schematicsDir(), c); if (exists(path.join(childDir, 'schema.json'))) { schematics[c] = { factory: `./${c}`, schema: `./${path.join(c, 'schema.json')}`, description: `Schematic ${c}` }; } }); return { name: 'workspace-schematics', version: '1.0', schematics }; } function saveCollection(dir: string, collection: any) { writeFileSync( path.join(dir, 'workspace-schematics.json'), JSON.stringify(collection) ); } function schematicsDir() { return path.join('tools', 'schematics'); } // prepareExecutionContext function prepareExecutionContext(outDir: string, schematicName: string) { const engineHost = new EngineHostHandlingWorkspaceSchematics(outDir); const engine = new SchematicEngine(engineHost); const schematic = engine .createCollection('workspace-schematics') .createSchematic(schematicName); const host = new virtualFs.ScopedHost( new NodeJsSyncHost(), normalize(rootDirectory) ); return { schematic, host, engine }; } // execute schematic function executeSchematic( schematicName: string, options: { [p: string]: any }, schematic: Schematic, host: virtualFs.Host, engine: SchematicEngine ) { const dryRunSink = new DryRunSink(host, true); let error = false; dryRunSink.reporter.subscribe((event: any) => { const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path; switch (event.kind) { case 'error': const desc = event.description == 'alreadyExist' ? 'already exists' : 'does not exist.'; console.error(`error! ${eventPath} ${desc}.`); error = true; break; case 'update': console.log(`update ${eventPath} (${event.content.length} bytes)`); break; case 'create': console.log(`create ${eventPath} (${event.content.length} bytes)`); break; case 'delete': console.log(`delete ${eventPath}`); break; case 'rename': const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to; console.log(`rename ${eventPath} => ${eventToPath}`); break; } }); const fsSink = new HostSink(host, true); schematic .call(options, of(new HostTree(host))) .pipe( map((tree: any) => Tree.optimize(tree)) as any, concatMap((tree: any) => dryRunSink .commit(tree) .pipe(ignoreElements() as any, concat(of(tree)) as any) ) as any, concatMap( (tree: any) => error ? of(tree) : fsSink .commit(tree) .pipe(ignoreElements() as any, concat(of(tree)) as any) ) as any, concatMap(() => (error ? [] : engine.executePostTasks())) as any ) .subscribe( () => {}, e => { console.error(`Error occurred while executing '${schematicName}':`); console.error(e); }, () => { console.log(`'${schematicName}' completed.`); } ); } /** * It uses FileSystemEngineHost for collection named "workspace-tools" and NodeModulesEngineHost * for everything else. */ class EngineHostHandlingWorkspaceSchematics implements EngineHost { readonly toolsHost: FileSystemEngineHost; readonly defaultHost: NodeModulesEngineHost; transformContext(): void {} constructor(outDir: string) { const transforms = validateOptionsWithSchema( new CoreSchemaRegistry(standardFormats) ); this.toolsHost = new FileSystemEngineHost(outDir); this.toolsHost.registerOptionsTransform(transforms); this.defaultHost = new NodeModulesEngineHost(); this.defaultHost.registerOptionsTransform(transforms); this.defaultHost.registerTaskExecutor(BuiltinTaskExecutor.NodePackage, { rootDirectory, packageManager: fileExists('yarn.lock') ? 'yarn' : 'npm' }); } createCollectionDescription(name: string): CollectionDescription { return this.hostFor(name).createCollectionDescription(name); } createSchematicDescription( name: string, collection: CollectionDescription ): SchematicDescription | null { return this.hostFor(collection.name).createSchematicDescription( name, collection ); } getSchematicRuleFactory( schematic: SchematicDescription, collection: CollectionDescription ): RuleFactory { return this.hostFor(collection.name).getSchematicRuleFactory( schematic, collection ); } createSourceFromUrl(url: Url, context: any): any { return this.hostFor(context.schematic.collection.name).createSourceFromUrl( url ); } transformOptions( schematic: SchematicDescription, options: OptionT ): any { return this.hostFor(schematic.collection.name).transformOptions( schematic, options ); } listSchematics(collection: any): string[] { return this.listSchematicNames(collection.description); } listSchematicNames(collection: CollectionDescription): string[] { return this.hostFor(collection.name).listSchematicNames(collection); } createTaskExecutor(name: string): any { return this.defaultHost.createTaskExecutor(name); } hasTaskExecutor(name: string): boolean { return this.defaultHost.hasTaskExecutor(name); } private hostFor(name: string) { return name === 'workspace-schematics' ? this.toolsHost : this.defaultHost; } } function parseOptions(args: string[]): { [k: string]: any } { const parsed = yargsParser(args); if (parsed._ && parsed._.length > 1) { parsed.name = parsed._[1]; } delete parsed._; return parsed; } function exists(file: string): boolean { try { return !!fs.statSync(file); } catch (e) { return false; } } function fileExists(filePath: string): boolean { try { return statSync(filePath).isFile(); } catch (err) { return false; } }