nx/packages/tao/src/commands/ngcli-adapter.ts

498 lines
14 KiB
TypeScript

import { Architect } from '@angular-devkit/architect';
import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node';
import {
json,
logging,
normalize,
Path,
schema,
tags,
virtualFs,
workspaces,
} from '@angular-devkit/core';
import * as chalk from 'chalk';
import { createConsoleLogger, NodeJsSyncHost } from '@angular-devkit/core/node';
import { RunOptions } from './run';
import {
FileSystemCollectionDescription,
FileSystemSchematicDescription,
NodeModulesEngineHost,
NodeWorkflow,
validateOptionsWithSchema,
} from '@angular-devkit/schematics/tools';
import {
DryRunEvent,
formats,
Schematic,
TaskExecutor,
} from '@angular-devkit/schematics';
import * as fs from 'fs';
import { readFileSync } from 'fs';
import { detectPackageManager } from '@nrwl/tao/src/shared/package-manager';
import { GenerateOptions } from './generate';
import * as taoTree from '../shared/tree';
import {
workspaceConfigName,
Workspaces,
} from '@nrwl/tao/src/shared/workspace';
import { BaseWorkflow } from '@angular-devkit/schematics/src/workflow';
import { NodePackageName } from '@angular-devkit/schematics/tasks/package-manager/options';
import { BuiltinTaskExecutor } from '@angular-devkit/schematics/tasks/node';
import { dirname, extname, join, resolve } from 'path';
import * as stripJsonComments from 'strip-json-comments';
import { FileBuffer } from '@angular-devkit/core/src/virtual-fs/host/interface';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { NX_ERROR, NX_PREFIX } from '../shared/logger';
export async function run(root: string, opts: RunOptions, verbose: boolean) {
const logger = getLogger(verbose);
const fsHost = new NxScopedHost(normalize(root));
const { workspace } = await workspaces.readWorkspace(
workspaceConfigName(root),
workspaces.createWorkspaceHost(fsHost)
);
const registry = new json.schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, root);
const architect = new Architect(architectHost, registry);
const run = await architect.scheduleTarget(
{
project: opts.project,
target: opts.target,
configuration: opts.configuration,
},
opts.runOptions,
{ logger }
);
const result = await run.output.toPromise();
await run.stop();
return result.success ? 0 : 1;
}
function createWorkflow(
fsHost: virtualFs.Host<fs.Stats>,
root: string,
opts: any
) {
const workflow = new NodeWorkflow(fsHost, {
force: opts.force,
dryRun: opts.dryRun,
packageManager: detectPackageManager(),
root: normalize(root),
registry: new schema.CoreSchemaRegistry(formats.standardFormats),
resolvePaths: [process.cwd(), root],
});
workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults);
workflow.engineHost.registerOptionsTransform(
validateOptionsWithSchema(workflow.registry)
);
return workflow;
}
function getCollection(workflow: any, name: string) {
const collection = workflow.engine.createCollection(name);
if (!collection) throw new Error(`Cannot find collection '${name}'`);
return collection;
}
function createRecorder(
record: {
loggingQueue: string[];
error: boolean;
},
logger: logging.Logger
) {
return (event: DryRunEvent) => {
const eventPath = event.path.startsWith('/')
? event.path.substr(1)
: event.path;
if (event.kind === 'error') {
record.error = true;
logger.warn(
`ERROR! ${eventPath} ${
event.description == 'alreadyExist'
? 'already exists'
: 'does not exist.'
}.`
);
} else if (event.kind === 'update') {
record.loggingQueue.push(
tags.oneLine`${chalk.white('UPDATE')} ${eventPath} (${
event.content.length
} bytes)`
);
} else if (event.kind === 'create') {
record.loggingQueue.push(
tags.oneLine`${chalk.green('CREATE')} ${eventPath} (${
event.content.length
} bytes)`
);
} else if (event.kind === 'delete') {
record.loggingQueue.push(`${chalk.yellow('DELETE')} ${eventPath}`);
} else if (event.kind === 'rename') {
record.loggingQueue.push(
`${chalk.blue('RENAME')} ${eventPath} => ${event.to}`
);
}
};
}
async function runSchematic(
root: string,
workflow: NodeWorkflow,
logger: logging.Logger,
opts: GenerateOptions,
schematic: Schematic<
FileSystemCollectionDescription,
FileSystemSchematicDescription
>,
printDryRunMessage = true,
recorder: any = null
): Promise<{ status: number; loggingQueue: string[] }> {
const record = { loggingQueue: [] as string[], error: false };
workflow.reporter.subscribe(recorder || createRecorder(record, logger));
try {
await workflow
.execute({
collection: opts.collectionName,
schematic: opts.generatorName,
options: opts.generatorOptions,
debug: opts.debug,
logger,
})
.toPromise();
} catch (e) {
console.log(e);
throw e;
}
if (!record.error) {
record.loggingQueue.forEach((log) => logger.info(log));
}
if (opts.dryRun && printDryRunMessage) {
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
}
return { status: 0, loggingQueue: record.loggingQueue };
}
class MigrationEngineHost extends NodeModulesEngineHost {
private nodeInstallLogPrinted = false;
constructor(logger: logging.Logger) {
super();
// Overwrite the original CLI node package executor with a new one that does basically nothing
// since nx migrate doesn't do npm installs by itself
// (https://github.com/angular/angular-cli/blob/5df776780deadb6be5048b3ab006a5d3383650dc/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts#L41)
this.registerTaskExecutor({
name: NodePackageName,
create: () =>
Promise.resolve<TaskExecutor>(() => {
return new Promise((res) => {
if (!this.nodeInstallLogPrinted) {
logger.warn(
`An installation of node_modules has been required. Make sure to run it after the migration`
);
this.nodeInstallLogPrinted = true;
}
res();
});
}),
});
this.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic);
}
protected _resolveCollectionPath(name: string): string {
let collectionPath: string | undefined = undefined;
if (name.startsWith('.') || name.startsWith('/')) {
name = resolve(name);
}
if (extname(name)) {
collectionPath = require.resolve(name);
} else {
const packageJsonPath = require.resolve(join(name, 'package.json'));
// eslint-disable-next-line @typescript-eslint/no-var-requires
const packageJson = require(packageJsonPath);
let pkgJsonSchematics = packageJson['nx-migrations'];
if (!pkgJsonSchematics) {
pkgJsonSchematics = packageJson['ng-update'];
if (!pkgJsonSchematics) {
throw new Error(`Could not find migrations in package: "${name}"`);
}
}
if (typeof pkgJsonSchematics != 'string') {
pkgJsonSchematics = pkgJsonSchematics.migrations;
}
collectionPath = resolve(dirname(packageJsonPath), pkgJsonSchematics);
}
try {
if (collectionPath) {
JSON.parse(stripJsonComments(readFileSync(collectionPath).toString()));
return collectionPath;
}
} catch (e) {
throw new Error(`Invalid migration file in package: "${name}"`);
}
throw new Error(`Collection cannot be resolved: "${name}"`);
}
}
class MigrationsWorkflow extends BaseWorkflow {
constructor(host: virtualFs.Host, logger: logging.Logger) {
super({
host,
engineHost: new MigrationEngineHost(logger),
force: true,
dryRun: false,
});
}
}
class NxScopedHost extends virtualFs.ScopedHost<any> {
constructor(root: Path) {
super(new NodeJsSyncHost(), root);
}
read(path: Path): Observable<FileBuffer> {
if (this.isWorkspaceConfig(path)) {
return this.isNewFormat().pipe(
switchMap((newFormat) => {
if (newFormat) {
return super.read(path).pipe(
map((r) => {
try {
const w = JSON.parse(Buffer.from(r).toString());
const formatted = new Workspaces().toOldFormatOrNull(w);
return formatted
? Buffer.from(JSON.stringify(formatted, null, 2))
: r;
} catch (e) {
return r;
}
})
);
} else {
return super.read(path);
}
})
);
} else {
return super.read(path);
}
}
write(path: Path, content: FileBuffer): Observable<void> {
if (this.isWorkspaceConfig(path)) {
return this.isNewFormat().pipe(
switchMap((newFormat) => {
if (newFormat) {
try {
const w = JSON.parse(Buffer.from(content).toString());
const formatted = new Workspaces().toNewFormatOrNull(w);
if (formatted) {
return super.write(
path,
Buffer.from(JSON.stringify(formatted, null, 2))
);
} else {
return super.write(path, content);
}
} catch (e) {
return super.write(path, content);
}
} else {
return super.write(path, content);
}
})
);
} else {
return super.write(path, content);
}
}
private isWorkspaceConfig(path: Path) {
const p = path.toString();
return (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
);
}
private isNewFormat() {
return super.exists('/angular.json' as any).pipe(
switchMap((isAngularJson) => {
return super
.read((isAngularJson ? '/angular.json' : '/workspace.json') as any)
.pipe(
map((r) => JSON.parse(Buffer.from(r).toString()).version === 2)
);
})
);
}
}
export async function generate(
root: string,
opts: GenerateOptions,
verbose: boolean
) {
const logger = getLogger(verbose);
const fsHost = new NxScopedHost(normalize(root));
const workflow = createWorkflow(fsHost, root, opts);
const collection = getCollection(workflow, opts.collectionName);
const schematic = collection.createSchematic(opts.generatorName, true);
return (
await runSchematic(
root,
workflow,
logger as any,
{ ...opts, generatorName: schematic.description.name },
schematic
)
).status;
}
export async function runMigration(
root: string,
collection: string,
schematic: string
) {
const host = new NxScopedHost(normalize(root));
const workflow = new MigrationsWorkflow(host, logger as any);
return workflow
.execute({
collection,
schematic,
options: {},
debug: false,
logger: logger as any,
})
.toPromise();
}
export function wrapAngularDevkitSchematic(
collectionName: string,
generatorName: string
) {
return async (host: taoTree.Tree, generatorOptions: { [k: string]: any }) => {
const emptyLogger = {
log: (e) => {},
info: (e) => {},
warn: (e) => {},
error: (e) => {},
fatal: (e) => {},
} as any;
emptyLogger.createChild = () => emptyLogger;
const recorder = (event: DryRunEvent) => {
const eventPath = event.path.startsWith('/')
? event.path.substr(1)
: event.path;
if (event.kind === 'error') {
} else if (event.kind === 'update') {
host.write(eventPath, event.content);
} else if (event.kind === 'create') {
host.write(eventPath, event.content);
} else if (event.kind === 'delete') {
host.delete(eventPath);
} else if (event.kind === 'rename') {
host.rename(eventPath, event.to);
}
};
const fsHost = new NxScopedHost(normalize(host.root));
await Promise.all(
(host as taoTree.FsTree).listChanges().map(async (c) => {
if (c.type === 'CREATE' || c.type === 'UPDATE') {
await fsHost.write(c.path as any, c.content).toPromise();
} else {
await fsHost.delete(c.path as any).toPromise();
}
})
);
const options = {
generatorOptions: { ...generatorOptions, _: [] },
dryRun: true,
interactive: false,
help: false,
debug: false,
collectionName,
generatorName,
force: false,
defaults: false,
};
const workflow = createWorkflow(fsHost, host.root, options);
const collection = getCollection(workflow, collectionName);
const schematic = collection.createSchematic(generatorName, true);
const res = await runSchematic(
host.root,
workflow,
emptyLogger,
options,
schematic,
false,
recorder
);
if (res.status !== 0) {
throw new Error(res.loggingQueue.join('\n'));
}
};
}
export async function invokeNew(
root: string,
opts: GenerateOptions,
verbose: boolean
) {
const logger = getLogger(verbose);
const fsHost = new NxScopedHost(normalize(root));
const workflow = createWorkflow(fsHost, root, opts);
const collection = getCollection(workflow, opts.collectionName);
const schematic = collection.createSchematic('new', true);
return (
await runSchematic(
root,
workflow,
logger as any,
{ ...opts, generatorName: schematic.description.name },
schematic
)
).status;
}
let logger: logging.Logger;
export const getLogger = (isVerbose = false): any => {
if (!logger) {
logger = createConsoleLogger(isVerbose, process.stdout, process.stderr, {
warn: (s) => chalk.bold(chalk.yellow(s)),
error: (s) => {
if (s.startsWith('NX ')) {
return `\n${NX_ERROR} ${chalk.bold(chalk.red(s.substr(3)))}\n`;
}
return chalk.bold(chalk.red(s));
},
info: (s) => {
if (s.startsWith('NX ')) {
return `\n${NX_PREFIX} ${chalk.bold(s.substr(3))}\n`;
}
return chalk.white(s);
},
});
}
return logger;
};