450 lines
12 KiB
TypeScript
450 lines
12 KiB
TypeScript
import * as chalk from 'chalk';
|
|
import { execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import { 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';
|
|
import { appRootPath } from '../utilities/app-root';
|
|
import {
|
|
detectPackageManager,
|
|
getPackageManagerCommand,
|
|
} from '@nrwl/tao/src/shared/package-manager';
|
|
import {
|
|
fileExists,
|
|
readJsonFile,
|
|
writeJsonFile,
|
|
} from '../utilities/fileutils';
|
|
import { output } from '../utilities/output';
|
|
import { CompilerOptions } from 'typescript';
|
|
import { Workspaces } from '@nrwl/tao/src/shared/workspace';
|
|
import { logger, normalizePath } from '@nrwl/devkit';
|
|
|
|
const rootDirectory = appRootPath;
|
|
|
|
type TsConfig = {
|
|
extends: string;
|
|
compilerOptions: CompilerOptions;
|
|
files?: string[];
|
|
include?: string[];
|
|
exclude?: string[];
|
|
references?: Array<{ path: string }>;
|
|
};
|
|
|
|
export async function workspaceGenerators(args: string[]) {
|
|
const outDir = compileTools();
|
|
const parsedArgs = parseOptions(args, outDir);
|
|
|
|
const collectionFile = path.join(outDir, 'workspace-generators.json');
|
|
if (parsedArgs.listGenerators) {
|
|
return listGenerators(collectionFile);
|
|
}
|
|
const generatorName = args[0];
|
|
const ws = new Workspaces(rootDirectory);
|
|
if (ws.isNxGenerator(collectionFile, generatorName)) {
|
|
try {
|
|
execSync(
|
|
`npx tao g "${collectionFile}":${generatorName} ${args
|
|
.slice(1)
|
|
.join(' ')}`,
|
|
{ stdio: ['inherit', 'inherit', 'inherit'] }
|
|
);
|
|
} catch (e) {
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
const logger = require('@angular-devkit/core/node').createConsoleLogger(
|
|
parsedArgs.verbose,
|
|
process.stdout,
|
|
process.stderr
|
|
);
|
|
try {
|
|
const workflow = createWorkflow(ws, parsedArgs.dryRun);
|
|
await executeAngularDevkitSchematic(
|
|
generatorName,
|
|
parsedArgs,
|
|
workflow,
|
|
outDir,
|
|
logger
|
|
);
|
|
} catch (e) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// compile tools
|
|
function compileTools() {
|
|
const toolsOutDir = getToolsOutDir();
|
|
removeSync(toolsOutDir);
|
|
compileToolsDir(toolsOutDir);
|
|
|
|
const generatorsOutDir = path.join(toolsOutDir, 'generators');
|
|
const collectionData = constructCollection();
|
|
saveCollection(generatorsOutDir, collectionData);
|
|
return generatorsOutDir;
|
|
}
|
|
|
|
function getToolsOutDir() {
|
|
return path.resolve(toolsDir(), toolsTsConfig().compilerOptions.outDir);
|
|
}
|
|
|
|
function compileToolsDir(outDir: string) {
|
|
copySync(generatorsDir(), path.join(outDir, 'generators'));
|
|
|
|
const tmpTsConfigPath = createTmpTsConfig(toolsTsConfigPath(), {
|
|
include: [path.join(generatorsDir(), '**/*.ts')],
|
|
});
|
|
|
|
const pmc = getPackageManagerCommand();
|
|
const tsc = `${pmc.exec} tsc`;
|
|
try {
|
|
execSync(`${tsc} -p ${tmpTsConfigPath}`, {
|
|
stdio: 'inherit',
|
|
cwd: rootDirectory,
|
|
});
|
|
} catch (e) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function constructCollection() {
|
|
const generators = {};
|
|
fs.readdirSync(generatorsDir()).forEach((c) => {
|
|
const childDir = path.join(generatorsDir(), c);
|
|
if (exists(path.join(childDir, 'schema.json'))) {
|
|
generators[c] = {
|
|
factory: `./${c}`,
|
|
schema: `./${normalizePath(path.join(c, 'schema.json'))}`,
|
|
description: `Schematic ${c}`,
|
|
};
|
|
}
|
|
});
|
|
return {
|
|
name: 'workspace-generators',
|
|
version: '1.0',
|
|
generators,
|
|
schematics: generators,
|
|
};
|
|
}
|
|
|
|
function saveCollection(dir: string, collection: any) {
|
|
writeFileSync(
|
|
path.join(dir, 'workspace-generators.json'),
|
|
JSON.stringify(collection)
|
|
);
|
|
}
|
|
|
|
function generatorsDir() {
|
|
return path.join(rootDirectory, 'tools', 'generators');
|
|
}
|
|
|
|
function toolsDir() {
|
|
return path.join(rootDirectory, 'tools');
|
|
}
|
|
|
|
function toolsTsConfigPath() {
|
|
return path.join(toolsDir(), 'tsconfig.tools.json');
|
|
}
|
|
|
|
function toolsTsConfig() {
|
|
return readJsonFile<TsConfig>(toolsTsConfigPath());
|
|
}
|
|
|
|
function createWorkflow(workspace: Workspaces, dryRun: boolean) {
|
|
const { virtualFs, schema } = require('@angular-devkit/core');
|
|
const { NodeJsSyncHost } = require('@angular-devkit/core/node');
|
|
const { formats } = require('@angular-devkit/schematics');
|
|
const { NodeWorkflow } = require('@angular-devkit/schematics/tools');
|
|
const root = normalizePath(rootDirectory);
|
|
const host = new virtualFs.ScopedHost(new NodeJsSyncHost(), root);
|
|
const workflow = new NodeWorkflow(host, {
|
|
packageManager: detectPackageManager(),
|
|
dryRun,
|
|
registry: new schema.CoreSchemaRegistry(formats.standardFormats),
|
|
resolvePaths: [process.cwd(), rootDirectory],
|
|
});
|
|
workflow.registry.addSmartDefaultProvider('projectName', () =>
|
|
workspace.calculateDefaultProjectName(
|
|
process.cwd(),
|
|
workspace.readWorkspaceConfiguration()
|
|
)
|
|
);
|
|
return workflow;
|
|
}
|
|
|
|
function listGenerators(collectionFile: string) {
|
|
try {
|
|
const bodyLines: string[] = [];
|
|
|
|
const collection = JSON.parse(fs.readFileSync(collectionFile).toString());
|
|
|
|
bodyLines.push(chalk.bold(chalk.green('WORKSPACE GENERATORS')));
|
|
bodyLines.push('');
|
|
bodyLines.push(
|
|
...Object.entries(collection.generators).map(
|
|
([schematicName, schematicMeta]: [string, any]) => {
|
|
return `${chalk.bold(schematicName)} : ${schematicMeta.description}`;
|
|
}
|
|
)
|
|
);
|
|
bodyLines.push('');
|
|
|
|
output.log({
|
|
title: '',
|
|
bodyLines,
|
|
});
|
|
} catch (error) {
|
|
logger.fatal(error.message);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function createPromptProvider(): any {
|
|
return (definitions: Array<any>) => {
|
|
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: !!definition.multiselect ? 'checkbox' : '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 executeAngularDevkitSchematic(
|
|
schematicName: string,
|
|
options: { [p: string]: any },
|
|
workflow: any,
|
|
outDir: string,
|
|
logger: any
|
|
) {
|
|
const { schema } = require('@angular-devkit/core');
|
|
const {
|
|
validateOptionsWithSchema,
|
|
} = require('@angular-devkit/schematics/tools');
|
|
const {
|
|
UnsuccessfulWorkflowExecution,
|
|
} = require('@angular-devkit/schematics');
|
|
|
|
output.logSingleLine(
|
|
`${output.colors.gray(`Executing your local schematic`)}: ${schematicName}`
|
|
);
|
|
|
|
let nothingDone = true;
|
|
let loggingQueue: string[] = [];
|
|
let hasError = false;
|
|
|
|
workflow.reporter.subscribe((event: any) => {
|
|
nothingDone = false;
|
|
const eventPath = event.path.startsWith('/')
|
|
? event.path.substr(1)
|
|
: event.path;
|
|
switch (event.kind) {
|
|
case 'error':
|
|
hasError = true;
|
|
|
|
const desc =
|
|
event.description == 'alreadyExist'
|
|
? 'already exists'
|
|
: 'does not exist.';
|
|
logger.warn(`ERROR! ${eventPath} ${desc}.`);
|
|
break;
|
|
case 'update':
|
|
loggingQueue.push(
|
|
`${chalk.white('UPDATE')} ${eventPath} (${
|
|
event.content.length
|
|
} bytes)`
|
|
);
|
|
break;
|
|
case 'create':
|
|
loggingQueue.push(
|
|
`${chalk.green('CREATE')} ${eventPath} (${
|
|
event.content.length
|
|
} bytes)`
|
|
);
|
|
break;
|
|
case 'delete':
|
|
loggingQueue.push(`${chalk.yellow('DELETE')} ${eventPath}`);
|
|
break;
|
|
case 'rename':
|
|
const eventToPath = event.to.startsWith('/')
|
|
? event.to.substr(1)
|
|
: event.to;
|
|
loggingQueue.push(
|
|
`${chalk.blue('RENAME')} ${eventPath} => ${eventToPath}`
|
|
);
|
|
break;
|
|
}
|
|
});
|
|
|
|
workflow.lifeCycle.subscribe((event) => {
|
|
if (event.kind === 'workflow-end' || event.kind === 'post-tasks-start') {
|
|
if (!hasError) {
|
|
loggingQueue.forEach((log) => logger.info(log));
|
|
}
|
|
|
|
loggingQueue = [];
|
|
hasError = false;
|
|
}
|
|
});
|
|
|
|
const args = options._.slice(1);
|
|
workflow.registry.addSmartDefaultProvider('argv', (schema: any) => {
|
|
if ('index' in schema) {
|
|
return args[+schema.index];
|
|
} else {
|
|
return args;
|
|
}
|
|
});
|
|
delete options._;
|
|
|
|
if (options.defaults) {
|
|
workflow.registry.addPreTransform(schema.transforms.addUndefinedDefaults);
|
|
} else {
|
|
workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults);
|
|
}
|
|
|
|
workflow.engineHost.registerOptionsTransform(
|
|
validateOptionsWithSchema(workflow.registry)
|
|
);
|
|
|
|
// Add support for interactive prompts
|
|
if (options.interactive) {
|
|
workflow.registry.usePromptProvider(createPromptProvider());
|
|
}
|
|
|
|
try {
|
|
await workflow
|
|
.execute({
|
|
collection: path.join(outDir, 'workspace-generators.json'),
|
|
schematic: schematicName,
|
|
options,
|
|
logger,
|
|
})
|
|
.toPromise();
|
|
|
|
if (nothingDone) {
|
|
logger.info('Nothing to be done.');
|
|
}
|
|
|
|
if (options.dryRun) {
|
|
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
|
}
|
|
} 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);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
function parseOptions(args: string[], outDir: string): { [k: string]: any } {
|
|
const schemaPath = path.join(outDir, args[0], 'schema.json');
|
|
let booleanProps = [];
|
|
if (fileExists(schemaPath)) {
|
|
const { properties } = readJsonFile(
|
|
path.join(outDir, args[0], 'schema.json')
|
|
);
|
|
if (properties) {
|
|
booleanProps = Object.keys(properties).filter(
|
|
(key) => properties[key].type === 'boolean'
|
|
);
|
|
}
|
|
}
|
|
return yargsParser(args, {
|
|
boolean: ['dryRun', 'listGenerators', 'interactive', ...booleanProps],
|
|
alias: {
|
|
dryRun: ['d'],
|
|
listSchematics: ['l'],
|
|
},
|
|
default: {
|
|
interactive: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
function exists(file: string): boolean {
|
|
try {
|
|
return !!fs.statSync(file);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createTmpTsConfig(
|
|
tsconfigPath: string,
|
|
updateConfig: Partial<TsConfig>
|
|
) {
|
|
const tmpTsConfigPath = path.join(
|
|
path.dirname(tsconfigPath),
|
|
'tsconfig.generated.json'
|
|
);
|
|
const originalTSConfig = readJsonFile<TsConfig>(tsconfigPath);
|
|
const generatedTSConfig: TsConfig = {
|
|
...originalTSConfig,
|
|
...updateConfig,
|
|
};
|
|
|
|
process.on('exit', () => {
|
|
cleanupTmpTsConfigFile(tmpTsConfigPath);
|
|
});
|
|
process.on('SIGTERM', () => {
|
|
cleanupTmpTsConfigFile(tmpTsConfigPath);
|
|
process.exit(0);
|
|
});
|
|
process.on('SIGINT', () => {
|
|
cleanupTmpTsConfigFile(tmpTsConfigPath);
|
|
process.exit(0);
|
|
});
|
|
|
|
writeJsonFile(tmpTsConfigPath, generatedTSConfig);
|
|
|
|
return tmpTsConfigPath;
|
|
}
|
|
|
|
function cleanupTmpTsConfigFile(tmpTsConfigPath) {
|
|
try {
|
|
if (tmpTsConfigPath) {
|
|
fs.unlinkSync(tmpTsConfigPath);
|
|
}
|
|
} catch (e) {}
|
|
}
|