nx/packages/schematics/src/command-line/workspace-schematic.ts

314 lines
8.3 KiB
TypeScript

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<any, any>,
host: virtualFs.Host,
engine: SchematicEngine<any, object>
) {
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<any, any> {
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<any> {
return this.hostFor(name).createCollectionDescription(name);
}
createSchematicDescription(
name: string,
collection: CollectionDescription<any>
): SchematicDescription<any, any> | null {
return this.hostFor(collection.name).createSchematicDescription(
name,
collection
);
}
getSchematicRuleFactory<OptionT extends object>(
schematic: SchematicDescription<any, any>,
collection: CollectionDescription<any>
): RuleFactory<OptionT> {
return this.hostFor(collection.name).getSchematicRuleFactory(
schematic,
collection
);
}
createSourceFromUrl(url: Url, context: any): any {
return this.hostFor(context.schematic.collection.name).createSourceFromUrl(
url
);
}
transformOptions<OptionT extends object>(
schematic: SchematicDescription<any, any>,
options: OptionT
): any {
return this.hostFor(schematic.collection.name).transformOptions(
schematic,
options
);
}
listSchematics(collection: any): string[] {
return this.listSchematicNames(collection.description);
}
listSchematicNames(collection: CollectionDescription<any>): 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;
}
}