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

283 lines
7.1 KiB
TypeScript

import {
JsonObject,
logging,
normalize,
schema,
virtualFs
} from '@angular-devkit/core';
import { createConsoleLogger, NodeJsSyncHost } from '@angular-devkit/core/node';
import {
SchematicEngine,
UnsuccessfulWorkflowExecution
} from '@angular-devkit/schematics';
import {
NodeModulesEngineHost,
NodeWorkflow
} from '@angular-devkit/schematics/tools';
import * as appRoot from 'app-root-path';
import { execSync } from 'child_process';
import * as fs from 'fs';
import { readFileSync, statSync, writeFileSync } from 'fs';
import { copySync, removeSync } from 'fs-extra';
import * as inquirer from 'inquirer';
import * as path from 'path';
import * as yargsParser from 'yargs-parser';
const rootDirectory = appRoot.path;
export function workspaceSchematic(args: string[]) {
const parsedArgs = parseOptions(args);
const logger = createConsoleLogger(
parsedArgs['verbose'],
process.stdout,
process.stderr
);
const outDir = compileTools();
if (parsedArgs['list-schematics']) {
return listSchematics(
path.join(outDir, 'workspace-schematics.json'),
logger
);
}
const schematicName = args[0];
const workflow = createWorkflow(parsedArgs.dryRun);
executeSchematic(schematicName, parsedArgs, workflow, outDir, logger);
}
// 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');
}
function createWorkflow(dryRun: boolean) {
const root = normalize(rootDirectory);
const host = new virtualFs.ScopedHost(new NodeJsSyncHost(), root);
return new NodeWorkflow(host, {
packageManager: fileExists('yarn.lock') ? 'yarn' : 'npm',
root,
dryRun
});
}
function listSchematics(collectionName: string, logger: logging.Logger) {
try {
const engineHost = new NodeModulesEngineHost();
const engine = new SchematicEngine(engineHost);
const collection = engine.createCollection(collectionName);
logger.info(engine.listSchematicNames(collection).join('\n'));
} catch (error) {
logger.fatal(error.message);
return 1;
}
return 0;
}
function createPromptProvider(): schema.PromptProvider {
return (definitions: Array<schema.PromptDefinition>) => {
const questions: inquirer.Questions = definitions.map(definition => {
const question: inquirer.Question = {
name: definition.id,
message: definition.message,
default: definition.default
};
const validator = definition.validator;
if (validator) {
question.validate = input => validator(input);
}
switch (definition.type) {
case 'confirmation':
return { ...question, type: 'confirm' };
case 'list':
return {
...question,
type: 'list',
choices:
definition.items &&
definition.items.map(item => {
if (typeof item == 'string') {
return item;
} else {
return {
name: item.label,
value: item.value
};
}
})
};
default:
return { ...question, type: definition.type };
}
});
return inquirer.prompt(questions);
};
}
// execute schematic
async function executeSchematic(
schematicName: string,
options: { [p: string]: any },
workflow: NodeWorkflow,
outDir: string,
logger: logging.Logger
) {
let nothingDone = true;
workflow.reporter.subscribe((event: any) => {
nothingDone = false;
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}.`);
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 args = options._.slice(1);
workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => {
if ('index' in schema) {
return args[+schema.index];
} else {
return args;
}
});
delete options._;
// Add support for interactive prompts
if (!options.noInteractive && options.interactive !== false) {
workflow.registry.usePromptProvider(createPromptProvider());
}
try {
await workflow
.execute({
collection: path.join(outDir, 'workspace-schematics.json'),
schematic: schematicName,
options: options,
logger: logger
})
.toPromise();
if (nothingDone) {
logger.info('Nothing to be done.');
}
} catch (err) {
if (err instanceof UnsuccessfulWorkflowExecution) {
// "See above" because we already printed the error.
logger.fatal('The Schematic workflow failed. See above.');
} else {
logger.fatal(err.stack || err.message);
}
}
}
function parseOptions(args: string[]): { [k: string]: any } {
return yargsParser(args, {
boolean: ['dryRun'],
alias: {
dryRun: ['d']
}
});
}
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;
}
}