985 lines
28 KiB
TypeScript
985 lines
28 KiB
TypeScript
/* eslint-disable no-restricted-imports */
|
|
import {
|
|
fragment,
|
|
logging,
|
|
normalize,
|
|
Path,
|
|
PathFragment,
|
|
schema,
|
|
tags,
|
|
virtualFs,
|
|
workspaces,
|
|
} from '@angular-devkit/core';
|
|
import * as chalk from 'chalk';
|
|
import { createConsoleLogger, NodeJsSyncHost } from '@angular-devkit/core/node';
|
|
import { Stats } from 'fs';
|
|
import { detectPackageManager } from '@nrwl/tao/src/shared/package-manager';
|
|
import { GenerateOptions } from './generate';
|
|
import { Tree } from '../shared/tree';
|
|
import {
|
|
inlineProjectConfigurations,
|
|
toNewFormat,
|
|
toNewFormatOrNull,
|
|
toOldFormatOrNull,
|
|
workspaceConfigName,
|
|
} from '../shared/workspace';
|
|
import { dirname, extname, resolve, join } from 'path';
|
|
import { FileBuffer } from '@angular-devkit/core/src/virtual-fs/host/interface';
|
|
import { EMPTY, Observable, of, concat } from 'rxjs';
|
|
import { catchError, map, switchMap, tap, toArray } from 'rxjs/operators';
|
|
import { NX_ERROR, NX_PREFIX } from '../shared/logger';
|
|
import { readJsonFile } from '../utils/fileutils';
|
|
import { parseJson, serializeJson } from '../utils/json';
|
|
|
|
export async function scheduleTarget(
|
|
root: string,
|
|
opts: {
|
|
project: string;
|
|
target: string;
|
|
configuration: string;
|
|
runOptions: any;
|
|
executor: string;
|
|
},
|
|
verbose: boolean
|
|
): Promise<Observable<import('@angular-devkit/architect').BuilderOutput>> {
|
|
const { Architect } = require('@angular-devkit/architect');
|
|
const {
|
|
WorkspaceNodeModulesArchitectHost,
|
|
} = require('@angular-devkit/architect/node');
|
|
|
|
const logger = getTargetLogger(opts.executor, verbose);
|
|
const fsHost = new NxScopedHost(normalize(root));
|
|
const { workspace } = await workspaces.readWorkspace(
|
|
workspaceConfigName(root),
|
|
workspaces.createWorkspaceHost(fsHost)
|
|
);
|
|
|
|
const registry = new 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 }
|
|
);
|
|
|
|
return run.output;
|
|
}
|
|
|
|
function createWorkflow(
|
|
fsHost: virtualFs.Host<Stats>,
|
|
root: string,
|
|
opts: any
|
|
) {
|
|
const NodeWorkflow = require('@angular-devkit/schematics/tools').NodeWorkflow;
|
|
const workflow = new NodeWorkflow(fsHost, {
|
|
force: opts.force,
|
|
dryRun: opts.dryRun,
|
|
packageManager: detectPackageManager(),
|
|
root: normalize(root),
|
|
registry: new schema.CoreSchemaRegistry(
|
|
require('@angular-devkit/schematics').formats.standardFormats
|
|
),
|
|
resolvePaths: [process.cwd(), root],
|
|
});
|
|
workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults);
|
|
workflow.engineHost.registerOptionsTransform(
|
|
require('@angular-devkit/schematics/tools').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;
|
|
}
|
|
|
|
async function createRecorder(
|
|
host: NxScopedHost,
|
|
record: {
|
|
loggingQueue: string[];
|
|
error: boolean;
|
|
},
|
|
logger: logging.Logger
|
|
) {
|
|
const actualConfigName = await host.workspaceConfigName();
|
|
return (event: import('@angular-devkit/schematics').DryRunEvent) => {
|
|
let eventPath = event.path.startsWith('/')
|
|
? event.path.substr(1)
|
|
: event.path;
|
|
|
|
if (eventPath === 'workspace.json' || eventPath === 'angular.json') {
|
|
eventPath = actualConfigName;
|
|
}
|
|
|
|
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}`
|
|
);
|
|
} else if (event.kind === 'create') {
|
|
record.loggingQueue.push(
|
|
tags.oneLine`${chalk.green('CREATE')} ${eventPath}`
|
|
);
|
|
} 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(
|
|
host: NxScopedHost,
|
|
root: string,
|
|
workflow: import('@angular-devkit/schematics/tools').NodeWorkflow,
|
|
logger: logging.Logger,
|
|
opts: GenerateOptions,
|
|
schematic: import('@angular-devkit/schematics').Schematic<
|
|
import('@angular-devkit/schematics/tools').FileSystemCollectionDescription,
|
|
import('@angular-devkit/schematics/tools').FileSystemSchematicDescription
|
|
>,
|
|
printDryRunMessage = true,
|
|
recorder: any = null
|
|
): Promise<{ status: number; loggingQueue: string[] }> {
|
|
const record = { loggingQueue: [] as string[], error: false };
|
|
workflow.reporter.subscribe(
|
|
recorder || (await createRecorder(host, 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 };
|
|
}
|
|
|
|
export class NxScopedHost extends virtualFs.ScopedHost<any> {
|
|
constructor(root: Path) {
|
|
super(new NodeJsSyncHost(), root);
|
|
}
|
|
|
|
read(path: Path): Observable<FileBuffer> {
|
|
return this.context(path).pipe(
|
|
switchMap((r) => {
|
|
if (r.isWorkspaceConfig) {
|
|
if (r.isNewFormat) {
|
|
return super.read(r.actualConfigFileName).pipe(
|
|
switchMap((r) => {
|
|
try {
|
|
const w = parseJson(Buffer.from(r).toString());
|
|
return this.resolveInlineProjectConfigurations(w).pipe(
|
|
map((w) => {
|
|
const formatted = toOldFormatOrNull(w);
|
|
return formatted
|
|
? Buffer.from(serializeJson(formatted))
|
|
: Buffer.from(serializeJson(w));
|
|
})
|
|
);
|
|
} catch (ex) {
|
|
return of(r);
|
|
}
|
|
})
|
|
);
|
|
} else {
|
|
return super.read(r.actualConfigFileName).pipe(
|
|
map((r) => {
|
|
const w = parseJson(Buffer.from(r).toString());
|
|
return Buffer.from(
|
|
serializeJson(inlineProjectConfigurations(w))
|
|
);
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
return super.read(path);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
write(path: Path, content: FileBuffer): Observable<void> {
|
|
return this.context(path).pipe(
|
|
switchMap((r) => {
|
|
if (r.isWorkspaceConfig) {
|
|
return this.writeWorkspaceConfiguration(r, content);
|
|
} else {
|
|
return super.write(path, content);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
isFile(path: Path): Observable<boolean> {
|
|
return this.context(path).pipe(
|
|
switchMap((r) => {
|
|
if (r.isWorkspaceConfig) {
|
|
return super.isFile(r.actualConfigFileName);
|
|
} else {
|
|
return super.isFile(path);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
exists(path: Path): Observable<boolean> {
|
|
return this.context(path).pipe(
|
|
switchMap((r) => {
|
|
if (r.isWorkspaceConfig) {
|
|
return super.exists(r.actualConfigFileName);
|
|
} else {
|
|
return super.exists(path);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
workspaceConfigName(): Promise<string> {
|
|
return super
|
|
.exists('/angular.json' as any)
|
|
.pipe(
|
|
map((hasAngularJson) =>
|
|
hasAngularJson ? 'angular.json' : 'workspace.json'
|
|
)
|
|
)
|
|
.toPromise();
|
|
}
|
|
|
|
protected context(path: string): Observable<{
|
|
isWorkspaceConfig: boolean;
|
|
actualConfigFileName: any;
|
|
isNewFormat: boolean;
|
|
}> {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
return super.exists('/angular.json' as any).pipe(
|
|
switchMap((isAngularJson) => {
|
|
const actualConfigFileName = isAngularJson
|
|
? '/angular.json'
|
|
: '/workspace.json';
|
|
return super.read(actualConfigFileName as any).pipe(
|
|
map((r) => {
|
|
try {
|
|
const w = parseJson(Buffer.from(r).toString());
|
|
return {
|
|
isWorkspaceConfig: true,
|
|
actualConfigFileName,
|
|
isNewFormat: w.version === 2,
|
|
};
|
|
} catch {
|
|
return {
|
|
isWorkspaceConfig: true,
|
|
actualConfigFileName,
|
|
isNewFormat: false,
|
|
};
|
|
}
|
|
})
|
|
);
|
|
})
|
|
);
|
|
} else {
|
|
return of({
|
|
isWorkspaceConfig: false,
|
|
actualConfigFileName: null,
|
|
isNewFormat: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
private writeWorkspaceConfiguration(context, content): Observable<void> {
|
|
const config = parseJson(Buffer.from(content).toString());
|
|
if (context.isNewFormat) {
|
|
try {
|
|
const w = parseJson(Buffer.from(content).toString());
|
|
const formatted = toNewFormatOrNull(w);
|
|
if (formatted) {
|
|
return this.writeWorkspaceConfigFiles(
|
|
context.actualConfigFileName,
|
|
formatted
|
|
);
|
|
} else {
|
|
return this.writeWorkspaceConfigFiles(
|
|
context.actualConfigFileName,
|
|
config
|
|
);
|
|
}
|
|
} catch (e) {
|
|
return this.writeWorkspaceConfigFiles(
|
|
context.actualConfigFileName,
|
|
config
|
|
);
|
|
}
|
|
} else {
|
|
return this.writeWorkspaceConfigFiles(
|
|
context.actualConfigFileName,
|
|
config
|
|
);
|
|
}
|
|
}
|
|
|
|
private writeWorkspaceConfigFiles(workspaceFileName, config) {
|
|
Object.entries(config.projects as Record<string, any>).forEach(
|
|
([project, config]) => {
|
|
if (config.configFilePath) {
|
|
const configPath = config.configFilePath;
|
|
const fileConfigObject = { ...config };
|
|
delete fileConfigObject.configFilePath;
|
|
super.write(configPath, Buffer.from(serializeJson(fileConfigObject)));
|
|
config.projects[project] = dirname(configPath);
|
|
}
|
|
}
|
|
);
|
|
return super.write(workspaceFileName, Buffer.from(serializeJson(config)));
|
|
}
|
|
|
|
protected resolveInlineProjectConfigurations(config: {
|
|
projects: Record<string, any>;
|
|
}): Observable<Object> {
|
|
let observable: Observable<any> = EMPTY;
|
|
Object.entries((config.projects as Record<string, any>) ?? {}).forEach(
|
|
([project, projectConfig]) => {
|
|
if (typeof projectConfig === 'string') {
|
|
const configFilePath = `${projectConfig}/project.json`;
|
|
const next = super.read(configFilePath as Path).pipe(
|
|
map((x) => ({
|
|
project,
|
|
projectConfig: {
|
|
...parseJson(Buffer.from(x).toString()),
|
|
configFilePath,
|
|
},
|
|
}))
|
|
);
|
|
observable = observable ? concat(observable, next) : next;
|
|
}
|
|
}
|
|
);
|
|
return observable.pipe(
|
|
toArray(),
|
|
map((x: any[]) => {
|
|
x.forEach(({ project, projectConfig }) => {
|
|
config.projects[project] = projectConfig;
|
|
});
|
|
return config;
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This host contains the workaround needed to run Angular migrations
|
|
*/
|
|
export class NxScopedHostForMigrations extends NxScopedHost {
|
|
constructor(root: Path) {
|
|
super(root);
|
|
}
|
|
|
|
read(path: Path): Observable<FileBuffer> {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
return super.read(path).pipe(map(processConfigWhenReading));
|
|
} else {
|
|
return super.read(path);
|
|
}
|
|
}
|
|
|
|
write(path: Path, content: FileBuffer) {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
return super.write(path, processConfigWhenWriting(content));
|
|
} else {
|
|
return super.write(path, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
|
|
constructor(root: Path, private readonly host: Tree) {
|
|
super(root);
|
|
}
|
|
|
|
read(path: Path): Observable<FileBuffer> {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
const match = findWorkspaceConfigFileChange(this.host);
|
|
// no match, default to existing behavior
|
|
if (!match) {
|
|
return super.read(path);
|
|
}
|
|
|
|
// we try to format it, if it changes, return it, otherwise return the original change
|
|
try {
|
|
const w = parseJson(Buffer.from(match.content).toString());
|
|
return this.resolveInlineProjectConfigurations(w).pipe(
|
|
map((x) => {
|
|
const formatted = toOldFormatOrNull(w);
|
|
return formatted
|
|
? Buffer.from(serializeJson(formatted))
|
|
: Buffer.from(serializeJson(x));
|
|
})
|
|
);
|
|
} catch (e) {
|
|
return super.read(path);
|
|
}
|
|
} else {
|
|
// found a matching change in the host
|
|
const match = findMatchingFileChange(this.host, path);
|
|
return match ? of(Buffer.from(match.content)) : super.read(path);
|
|
}
|
|
}
|
|
|
|
exists(path: Path): Observable<boolean> {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
return findWorkspaceConfigFileChange(this.host)
|
|
? of(true)
|
|
: super.exists(path);
|
|
} else {
|
|
return findMatchingFileChange(this.host, path)
|
|
? of(true)
|
|
: super.exists(path);
|
|
}
|
|
}
|
|
|
|
isDirectory(path: Path): Observable<boolean> {
|
|
return super.isDirectory(path).pipe(
|
|
catchError(() => of(false)),
|
|
switchMap((isDirectory) =>
|
|
isDirectory
|
|
? of(true)
|
|
: of(this.host.exists(path) && !this.host.isFile(path))
|
|
)
|
|
);
|
|
}
|
|
|
|
isFile(path: Path): Observable<boolean> {
|
|
if (isWorkspaceConfigPath(path)) {
|
|
return findWorkspaceConfigFileChange(this.host)
|
|
? of(true)
|
|
: super.isFile(path);
|
|
} else {
|
|
return findMatchingFileChange(this.host, path)
|
|
? of(true)
|
|
: super.isFile(path);
|
|
}
|
|
}
|
|
|
|
list(path: Path): Observable<PathFragment[]> {
|
|
const fragments = this.host.children(path).map((child) => fragment(child));
|
|
return of(fragments);
|
|
}
|
|
}
|
|
|
|
function findWorkspaceConfigFileChange(host: Tree) {
|
|
return host
|
|
.listChanges()
|
|
.find((f) => f.path == 'workspace.json' || f.path == 'angular.json');
|
|
}
|
|
|
|
function findMatchingFileChange(host: Tree, path: Path) {
|
|
const targetPath = path.startsWith('/') ? path.substring(1) : path.toString;
|
|
return host
|
|
.listChanges()
|
|
.find((f) => f.path == targetPath.toString() && f.type !== 'DELETE');
|
|
}
|
|
|
|
function isWorkspaceConfigPath(p: Path | string) {
|
|
return (
|
|
p === 'angular.json' ||
|
|
p === '/angular.json' ||
|
|
p === 'workspace.json' ||
|
|
p === '/workspace.json'
|
|
);
|
|
}
|
|
|
|
function processConfigWhenReading(content: ArrayBuffer) {
|
|
try {
|
|
const json = parseJson(Buffer.from(content).toString());
|
|
Object.values(json.projects).forEach((p: any) => {
|
|
try {
|
|
Object.values(p.architect || p.targets).forEach((e: any) => {
|
|
if (
|
|
(e.builder === '@nrwl/jest:jest' ||
|
|
e.executor === '@nrwl/jest:jest') &&
|
|
!e.options.tsConfig
|
|
) {
|
|
e.options.tsConfig = `${p.root}/tsconfig.spec.json`;
|
|
}
|
|
});
|
|
} catch (e) {}
|
|
});
|
|
return Buffer.from(serializeJson(json));
|
|
} catch (e) {
|
|
return content;
|
|
}
|
|
}
|
|
|
|
function processConfigWhenWriting(content: ArrayBuffer) {
|
|
try {
|
|
const json = parseJson(Buffer.from(content).toString());
|
|
Object.values(json.projects).forEach((p: any) => {
|
|
try {
|
|
Object.values(p.architect || p.targets).forEach((e: any) => {
|
|
if (
|
|
(e.builder === '@nrwl/jest:jest' ||
|
|
e.executor === '@nrwl/jest:jest') &&
|
|
e.options.tsConfig
|
|
) {
|
|
delete e.options.tsConfig;
|
|
}
|
|
});
|
|
} catch (e) {}
|
|
});
|
|
return Buffer.from(serializeJson(json));
|
|
} catch (e) {
|
|
return content;
|
|
}
|
|
}
|
|
|
|
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(
|
|
fsHost,
|
|
root,
|
|
workflow,
|
|
logger as any,
|
|
{ ...opts, generatorName: schematic.description.name },
|
|
schematic
|
|
)
|
|
).status;
|
|
}
|
|
|
|
export async function runMigration(
|
|
root: string,
|
|
collection: string,
|
|
schematic: string,
|
|
isVerbose: boolean
|
|
) {
|
|
const NodeModulesEngineHost =
|
|
require('@angular-devkit/schematics/tools').NodeModulesEngineHost;
|
|
|
|
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: require('@angular-devkit/schematics/tasks/package-manager/options')
|
|
.NodePackageName,
|
|
create: () =>
|
|
Promise.resolve<import('@angular-devkit/schematics').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(
|
|
require('@angular-devkit/schematics/tasks/node').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 {
|
|
let packageJsonPath;
|
|
try {
|
|
packageJsonPath = require.resolve(join(name, 'package.json'), {
|
|
paths: [process.cwd()],
|
|
});
|
|
} catch (e) {
|
|
// workaround for a bug in node 12
|
|
packageJsonPath = require.resolve(
|
|
join(process.cwd(), name, 'package.json')
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const packageJson = require(packageJsonPath);
|
|
let pkgJsonSchematics =
|
|
packageJson['nx-migrations'] ?? packageJson['ng-update'];
|
|
if (!pkgJsonSchematics) {
|
|
throw new Error(`Could not find migrations in package: "${name}"`);
|
|
}
|
|
if (typeof pkgJsonSchematics != 'string') {
|
|
pkgJsonSchematics = pkgJsonSchematics.migrations;
|
|
}
|
|
collectionPath = require.resolve(pkgJsonSchematics, {
|
|
paths: [dirname(packageJsonPath)],
|
|
});
|
|
}
|
|
|
|
try {
|
|
if (collectionPath) {
|
|
readJsonFile(collectionPath);
|
|
return collectionPath;
|
|
}
|
|
} catch {
|
|
throw new Error(`Invalid migration file in package: "${name}"`);
|
|
}
|
|
throw new Error(`Collection cannot be resolved: "${name}"`);
|
|
}
|
|
}
|
|
|
|
const { BaseWorkflow } = require('@angular-devkit/schematics/src/workflow');
|
|
|
|
class MigrationsWorkflow extends BaseWorkflow {
|
|
constructor(host: virtualFs.Host, logger: logging.Logger) {
|
|
super({
|
|
host,
|
|
engineHost: new MigrationEngineHost(logger) as any,
|
|
force: true,
|
|
dryRun: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
const logger = getLogger(isVerbose);
|
|
const host = new NxScopedHostForMigrations(normalize(root));
|
|
const workflow = new MigrationsWorkflow(host, logger as any);
|
|
return workflow
|
|
.execute({
|
|
collection,
|
|
schematic,
|
|
options: {},
|
|
debug: false,
|
|
logger: logger as any,
|
|
})
|
|
.toPromise();
|
|
}
|
|
|
|
function convertEventTypeToHandleMultipleConfigNames(
|
|
host: Tree,
|
|
eventPath: string,
|
|
content: Buffer | never
|
|
) {
|
|
const actualConfigName = host.exists('/workspace.json')
|
|
? 'workspace.json'
|
|
: 'angular.json';
|
|
const isWorkspaceConfig =
|
|
eventPath === 'angular.json' || eventPath === 'workspace.json';
|
|
if (isWorkspaceConfig) {
|
|
let isNewFormat = true;
|
|
try {
|
|
isNewFormat =
|
|
parseJson(host.read(actualConfigName, 'utf-8')).version === 2;
|
|
} catch (e) {}
|
|
|
|
if (content && isNewFormat) {
|
|
const formatted = toNewFormat(parseJson(content.toString()));
|
|
if (formatted) {
|
|
return {
|
|
eventPath: actualConfigName,
|
|
content: Buffer.from(serializeJson(formatted)),
|
|
};
|
|
} else {
|
|
return { eventPath: actualConfigName, content };
|
|
}
|
|
} else {
|
|
return { eventPath: actualConfigName, content };
|
|
}
|
|
} else {
|
|
return { eventPath, content };
|
|
}
|
|
}
|
|
|
|
let collectionResolutionOverrides = null;
|
|
let mockedSchematics = null;
|
|
|
|
/**
|
|
* By default, Angular Devkit schematic collections will be resolved using the Node resolution.
|
|
* This doesn't work if you are testing schematics that refer to other schematics in the
|
|
* same repo.
|
|
*
|
|
* This function can can be used to override the resolution behaviour.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```typescript
|
|
* overrideCollectionResolutionForTesting({
|
|
* '@nrwl/workspace': path.join(__dirname, '../../../../workspace/collection.json'),
|
|
* '@nrwl/angular': path.join(__dirname, '../../../../angular/collection.json'),
|
|
* '@nrwl/linter': path.join(__dirname, '../../../../linter/collection.json')
|
|
* });
|
|
*
|
|
* ```
|
|
*/
|
|
export function overrideCollectionResolutionForTesting(collections: {
|
|
[name: string]: string;
|
|
}) {
|
|
collectionResolutionOverrides = collections;
|
|
}
|
|
|
|
/**
|
|
* If you have an Nx Devkit generator invoking the wrapped Angular Devkit schematic,
|
|
* and you don't want the Angular Devkit schematic to run, you can mock it up using this function.
|
|
*
|
|
* Unfortunately, there are some edge cases in the Nx-Angular devkit integration that
|
|
* can be seen in the unit tests context. This function is useful for handling that as well.
|
|
*
|
|
* In this case, you can mock it up.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```typescript
|
|
* mockSchematicsForTesting({
|
|
* 'mycollection:myschematic': (tree, params) => {
|
|
* tree.write('README.md');
|
|
* }
|
|
* });
|
|
*
|
|
* ```
|
|
*/
|
|
export function mockSchematicsForTesting(schematics: {
|
|
[name: string]: (
|
|
host: Tree,
|
|
generatorOptions: { [k: string]: any }
|
|
) => Promise<void>;
|
|
}) {
|
|
mockedSchematics = schematics;
|
|
}
|
|
|
|
export function wrapAngularDevkitSchematic(
|
|
collectionName: string,
|
|
generatorName: string
|
|
) {
|
|
return async (host: Tree, generatorOptions: { [k: string]: any }) => {
|
|
if (
|
|
mockedSchematics &&
|
|
mockedSchematics[`${collectionName}:${generatorName}`]
|
|
) {
|
|
return await mockedSchematics[`${collectionName}:${generatorName}`](
|
|
host,
|
|
generatorOptions
|
|
);
|
|
}
|
|
|
|
const emptyLogger = {
|
|
log: (e) => {},
|
|
info: (e) => {},
|
|
warn: (e) => {},
|
|
debug: () => {},
|
|
error: (e) => {},
|
|
fatal: (e) => {},
|
|
} as any;
|
|
emptyLogger.createChild = () => emptyLogger;
|
|
|
|
const recorder = (
|
|
event: import('@angular-devkit/schematics').DryRunEvent
|
|
) => {
|
|
let eventPath = event.path.startsWith('/')
|
|
? event.path.substr(1)
|
|
: event.path;
|
|
|
|
const r = convertEventTypeToHandleMultipleConfigNames(
|
|
host,
|
|
eventPath,
|
|
(event as any).content
|
|
);
|
|
|
|
if (event.kind === 'error') {
|
|
} else if (event.kind === 'update') {
|
|
host.write(r.eventPath, r.content);
|
|
} else if (event.kind === 'create') {
|
|
host.write(r.eventPath, r.content);
|
|
} else if (event.kind === 'delete') {
|
|
host.delete(r.eventPath);
|
|
} else if (event.kind === 'rename') {
|
|
host.rename(r.eventPath, event.to);
|
|
}
|
|
};
|
|
|
|
const fsHost = new NxScopeHostUsedForWrappedSchematics(
|
|
normalize(host.root),
|
|
host
|
|
);
|
|
|
|
const options = {
|
|
generatorOptions,
|
|
dryRun: true,
|
|
interactive: false,
|
|
help: false,
|
|
debug: false,
|
|
collectionName,
|
|
generatorName,
|
|
force: false,
|
|
defaults: false,
|
|
};
|
|
const workflow = createWorkflow(fsHost, host.root, options);
|
|
|
|
// used for testing
|
|
if (collectionResolutionOverrides) {
|
|
const r = workflow.engineHost.resolve;
|
|
workflow.engineHost.resolve = (collection, b, c) => {
|
|
if (collectionResolutionOverrides[collection]) {
|
|
return collectionResolutionOverrides[collection];
|
|
} else {
|
|
return r.apply(workflow.engineHost, [collection, b, c]);
|
|
}
|
|
};
|
|
}
|
|
|
|
const collection = getCollection(workflow, collectionName);
|
|
const schematic = collection.createSchematic(generatorName, true);
|
|
const res = await runSchematic(
|
|
fsHost,
|
|
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(
|
|
fsHost,
|
|
root,
|
|
workflow,
|
|
logger as any,
|
|
{ ...opts, generatorName: schematic.description.name },
|
|
schematic
|
|
)
|
|
).status;
|
|
}
|
|
|
|
let logger: logging.Logger;
|
|
|
|
const loggerColors: Partial<Record<logging.LogLevel, (s: string) => string>> = {
|
|
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);
|
|
},
|
|
};
|
|
|
|
export const getLogger = (isVerbose = false): logging.Logger => {
|
|
if (!logger) {
|
|
logger = createConsoleLogger(
|
|
isVerbose,
|
|
process.stdout,
|
|
process.stderr,
|
|
loggerColors
|
|
);
|
|
}
|
|
return logger;
|
|
};
|
|
|
|
const getTargetLogger = (
|
|
executor: string,
|
|
isVerbose = false
|
|
): logging.Logger => {
|
|
if (executor !== '@angular-devkit/build-angular:tslint') {
|
|
return getLogger(isVerbose);
|
|
}
|
|
|
|
const tslintExecutorLogger = createConsoleLogger(
|
|
isVerbose,
|
|
process.stdout,
|
|
process.stderr,
|
|
{
|
|
...loggerColors,
|
|
warn: (s) => {
|
|
if (
|
|
s.startsWith(
|
|
`TSLint's support is discontinued and we're deprecating its support in Angular CLI.`
|
|
)
|
|
) {
|
|
s =
|
|
`TSLint's support is discontinued and the @angular-devkit/build-angular:tslint executor is deprecated.\n` +
|
|
'To start using a modern linter tool, please consider replacing TSLint with ESLint. ' +
|
|
'You can use the "@nrwl/angular:convert-tslint-to-eslint" generator to automatically convert your projects.\n' +
|
|
'For more info, visit https://nx.dev/latest/angular/angular/convert-tslint-to-eslint.';
|
|
}
|
|
return chalk.bold(chalk.yellow(s));
|
|
},
|
|
}
|
|
);
|
|
return tslintExecutorLogger;
|
|
};
|