nx/packages/nx/src/adapter/ngcli-adapter.ts
2022-05-31 15:35:28 -04:00

1254 lines
37 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 '../utils/package-manager';
import { GenerateOptions } from '../command-line/generate';
import { FileChange, Tree } from '../generators/tree';
import {
buildWorkspaceConfigurationFromGlobs,
globForProjectFiles,
toNewFormat,
toNewFormatOrNull,
toOldFormatOrNull,
workspaceConfigName,
} from '../config/workspaces';
import { dirname, extname, resolve, join, basename } from 'path';
import { FileBuffer } from '@angular-devkit/core/src/virtual-fs/host/interface';
import type { Architect } from '@angular-devkit/architect';
import { Observable, of, merge, forkJoin, NEVER } from 'rxjs';
import { catchError, map, switchMap, toArray, tap } from 'rxjs/operators';
import { NX_ERROR, NX_PREFIX } from '../utils/logger';
import { readJsonFile } from '../utils/fileutils';
import { parseJson, serializeJson } from '../utils/json';
import { NxJsonConfiguration } from '../config/nx-json';
import {
ProjectConfiguration,
RawProjectsConfigurations,
ProjectsConfigurations,
} from '../config/workspace-json-project-json';
import { readNxJson } from '../generators/utils/project-configuration';
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(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: Architect = new Architect(architectHost, registry);
const run = await architect.scheduleTarget(
{
project: opts.project,
target: opts.target,
configuration: opts.configuration,
},
opts.runOptions,
{ logger }
);
let lastOutputError: string;
return run.output.pipe(
tap(
(output) =>
(lastOutputError = !output.success ? output.error : undefined),
(error) => {}, // do nothing, this could be an intentional error
() => {
lastOutputError ? logger.error(lastOutputError) : 0;
}
)
);
}
function createWorkflow(
fsHost: virtualFs.Host<Stats>,
root: string,
opts: any
) {
const NodeWorkflow = require('@angular-devkit/schematics/tools').NodeWorkflow;
const workflow = new NodeWorkflow(fsHost, {
force: false,
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
)
);
if (opts.interactive) {
workflow.registry.usePromptProvider(createPromptProvider());
}
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.slice(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: false,
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 };
}
type AngularJsonConfiguration = ProjectsConfigurations &
Pick<NxJsonConfiguration, 'cli' | 'defaultProject' | 'generators'> & {
schematics?: NxJsonConfiguration['generators'];
cli?: NxJsonConfiguration['cli'] & {
schematicCollections?: string[];
};
};
export class NxScopedHost extends virtualFs.ScopedHost<any> {
protected __nxInMemoryWorkspace: ProjectsConfigurations | null;
constructor(private root: string) {
super(new NodeJsSyncHost(), normalize(root));
}
protected __readWorkspaceConfiguration = (
configFileName: ChangeContext['actualConfigFileName'],
overrides?: {
workspace?: Observable<RawProjectsConfigurations>;
nx?: Observable<NxJsonConfiguration>;
}
): Observable<FileBuffer> => {
const readJsonFile = (path: string) =>
super
.read(path as Path)
.pipe(map((data) => JSON.parse(Buffer.from(data).toString())));
const readWorkspaceJsonFile = (
nxJson: NxJsonConfiguration
): Observable<RawProjectsConfigurations> => {
if (overrides?.workspace) {
return overrides.workspace;
} else if (this.__nxInMemoryWorkspace) {
return of(this.__nxInMemoryWorkspace);
} else {
if (configFileName) {
return super
.read(configFileName)
.pipe(map((data) => parseJson(Buffer.from(data).toString())));
} else {
const staticProjects = globForProjectFiles(this.root);
this.__nxInMemoryWorkspace = buildWorkspaceConfigurationFromGlobs(
nxJson,
staticProjects.filter((x) => basename(x) !== 'package.json')
);
Object.entries(this.__nxInMemoryWorkspace.projects).forEach(
([project, config]) => {
this.__nxInMemoryWorkspace.projects[project] = config.root as any;
}
);
return of(this.__nxInMemoryWorkspace);
}
}
};
const readNxJsonFile = () => {
let nxJson = overrides?.nx ? overrides.nx : readJsonFile('nx.json');
return nxJson.pipe(
map((json) => {
if (json.extends) {
return { ...require(json.extends), ...json };
} else {
return json;
}
})
);
};
return super.exists('nx.json' as Path).pipe(
switchMap((nxJsonExists) => {
let nxJsonObservable: Observable<NxJsonConfiguration> = NEVER;
if (nxJsonExists) {
nxJsonObservable = readNxJsonFile();
} else {
nxJsonObservable = of({} as NxJsonConfiguration);
}
const workspaceJsonObservable: Observable<RawProjectsConfigurations> =
nxJsonObservable.pipe(switchMap((x) => readWorkspaceJsonFile(x)));
return forkJoin([nxJsonObservable, workspaceJsonObservable]);
}),
switchMap(([nxJson, workspaceJson]) => {
try {
// resolve inline configurations and downlevel format
return this.resolveInlineProjectConfigurations(workspaceJson).pipe(
map((x) => {
const angularJson: AngularJsonConfiguration = x;
// assign props ng cli expects from nx json, if it exists
angularJson.cli ??= nxJson?.cli;
angularJson.generators ??= nxJson?.generators;
angularJson.defaultProject ??= nxJson?.defaultProject;
if (workspaceJson.version === 2) {
const formatted = toOldFormatOrNull(workspaceJson);
return formatted
? Buffer.from(serializeJson(formatted))
: Buffer.from(serializeJson(x));
}
return Buffer.from(serializeJson(x));
})
);
} catch {
return of(Buffer.from(serializeJson(workspaceJson)));
}
})
);
};
read(path: Path): Observable<FileBuffer> {
return this.context(path).pipe(
switchMap((r) => {
if (r.isWorkspaceConfig) {
return this.__readWorkspaceConfiguration(r.actualConfigFileName);
} 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 of(true); // isWorkspaceConfig means its a file
} else {
return super.isFile(path);
}
})
);
}
exists(path: Path): Observable<boolean> {
return this.context(path).pipe(
switchMap((r) => {
if (r.isWorkspaceConfig) {
return of(true);
} 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<ChangeContext> {
if (isWorkspaceConfigPath(path)) {
return forkJoin([
super.exists('/angular.json' as Path),
super.exists('/workspace.json' as Path),
]).pipe(
switchMap(([isAngularJson, isWorkspaceJson]) => {
if (!isAngularJson && !isWorkspaceJson) {
return of({
isWorkspaceConfig: true,
actualConfigFileName: null,
// AngularJson / WorkspaceJson v2 is always used for standalone project config
isNewFormat: true,
});
}
const actualConfigFileName = isAngularJson
? ('/angular.json' as Path)
: ('/workspace.json' as Path);
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: ChangeContext,
content
): Observable<void> {
const config = parseJson(Buffer.from(content).toString());
if (context.isNewFormat) {
try {
const w = parseJson(Buffer.from(content).toString());
const formatted: AngularJsonConfiguration = toNewFormatOrNull(w);
if (formatted) {
const { cli, generators, defaultProject, ...workspaceJson } =
formatted;
delete cli.schematicCollections;
return merge(
this.writeWorkspaceConfigFiles(context, workspaceJson),
cli || generators || defaultProject
? this.__saveNxJsonProps({ cli, generators, defaultProject })
: of(null)
);
} else {
const {
cli,
schematics,
generators,
defaultProject,
...angularJson
} = w;
delete cli.schematicCollections;
return merge(
this.writeWorkspaceConfigFiles(context, angularJson),
cli || schematics
? this.__saveNxJsonProps({
cli,
defaultProject,
generators: schematics || generators,
})
: of(null)
);
}
} catch (e) {}
}
const { cli, schematics, generators, defaultProject, ...angularJson } =
config;
delete cli.schematicCollections;
return merge(
this.writeWorkspaceConfigFiles(context, angularJson),
this.__saveNxJsonProps({
cli,
defaultProject,
generators: schematics || generators,
})
);
}
private __saveNxJsonProps(
props: Partial<NxJsonConfiguration>
): Observable<void> {
const nxJsonPath = 'nx.json' as Path;
return super.read(nxJsonPath).pipe(
switchMap((buf) => {
const nxJson = parseJson(Buffer.from(buf).toString());
Object.assign(nxJson, props);
return super.write(nxJsonPath, Buffer.from(serializeJson(nxJson)));
})
);
}
private writeWorkspaceConfigFiles(
{ actualConfigFileName: workspaceFileName, isNewFormat }: ChangeContext,
config
) {
// copy to avoid removing inlined config files.
const writeObservables: Observable<void>[] = [];
const configToWrite = {
...config,
projects: { ...config.projects },
};
const projects: [string, any][] = Object.entries(configToWrite.projects);
for (const [project, projectConfig] of projects) {
if (projectConfig.configFilePath) {
if (workspaceFileName && !isNewFormat) {
throw new Error(
'Attempted to write standalone project configuration into a v1 workspace'
);
}
// project was read from a project.json file
const configPath = projectConfig.configFilePath;
const fileConfigObject = { ...projectConfig };
delete fileConfigObject.root; // remove the root before writing
delete fileConfigObject.configFilePath; // remove the configFilePath before writing
const projectJsonWrite = super.write(
configPath,
Buffer.from(serializeJson(fileConfigObject))
); // write back to the project.json file
writeObservables.push(projectJsonWrite);
configToWrite.projects[project] = normalize(dirname(configPath)); // update the config object to point to the written file.
}
}
if (workspaceFileName) {
const workspaceJsonWrite = super.write(
workspaceFileName,
Buffer.from(serializeJson(configToWrite))
);
writeObservables.push(workspaceJsonWrite);
}
return merge(...writeObservables);
}
protected resolveInlineProjectConfigurations(
config: RawProjectsConfigurations
): Observable<ProjectsConfigurations> {
// Creates an observable where each emission is a project configuration
// that is not listed inside workspace.json. Each time it encounters a
// standalone config, observable is updated by concatenating the new
// config read operation.
const observables: Observable<{
project: string;
projectConfig: ProjectConfiguration;
}>[] = [];
Object.entries((config.projects as Record<string, any>) ?? {}).forEach(
([project, projectConfig]) => {
if (typeof projectConfig === 'string') {
// configFilePath is not written to files, but is stored on the config object
// so that we know where to save the project's configuration if it was updated
// by another angular schematic.
const configFilePath = join(projectConfig, 'project.json');
const next = this.read(configFilePath as Path).pipe(
map((x) => ({
project,
projectConfig: {
root: dirname(configFilePath),
...parseJson(Buffer.from(x).toString()),
configFilePath,
},
}))
);
observables.push(next);
}
}
);
return merge(...observables).pipe(
toArray(),
map((configs) => {
configs.forEach(({ project, projectConfig }) => {
config.projects[project] = projectConfig;
});
return config as ProjectsConfigurations;
})
);
}
}
type ConfigFilePath = ('/angular.json' | '/workspace.json') & {
__PRIVATE_DEVKIT_PATH: void;
};
type ChangeContext = {
isWorkspaceConfig: boolean;
actualConfigFileName: ConfigFilePath | null;
isNewFormat: boolean;
};
export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
constructor(root: string, private readonly host: Tree) {
super(root);
}
read(path: Path): Observable<FileBuffer> {
if (isWorkspaceConfigPath(path)) {
const nxJsonChange = findMatchingFileChange(this.host, 'nx.json' as Path);
const match = findWorkspaceConfigFileChange(this.host);
let workspaceJsonOverride: Observable<RawProjectsConfigurations>;
let actualConfigFileName: ConfigFilePath = [
'/workspace.json',
'/angular.json',
].filter((f) => this.host.exists(f))[0] as ConfigFilePath;
if (actualConfigFileName) {
if (match) {
workspaceJsonOverride = of(parseJson(match.content.toString()));
}
} else if (!this.__nxInMemoryWorkspace) {
// if we've already dealt with this, let NxScopedHost read the cache...
// projects created inside a generator will not be visible
// to glob when it runs in nx/shared/workspace, so
// we have to add them into the file.
const createdProjectFiles = findCreatedProjects(this.host);
const deletedProjectFiles = findDeletedProjects(this.host);
const nxJsonInTree = readNxJson(this.host);
const readJsonWithHost = (file) => ({
root: dirname(file),
...parseJson(this.host.read(file).toString()),
});
const staticProjects = buildWorkspaceConfigurationFromGlobs(
nxJsonInTree,
globForProjectFiles(this.host.root).filter(
(x) => basename(x) !== 'package.json'
),
readJsonWithHost
);
const createdProjects = buildWorkspaceConfigurationFromGlobs(
nxJsonInTree,
createdProjectFiles.map((x) => x.path),
readJsonWithHost
).projects;
deletedProjectFiles.forEach((file) => {
const matchingStaticProject = Object.entries(
staticProjects.projects
).find(([, config]) => config.root === dirname(file.path));
if (matchingStaticProject) {
delete staticProjects.projects[matchingStaticProject[0]];
}
});
const workspace = {
...staticProjects,
projects: { ...staticProjects.projects, ...createdProjects },
};
workspaceJsonOverride = of({
...workspace,
// all projects **must** be standalone if workspace.json doesn't exist
// since the NxScopedHost already handles the standalone config case,
// lets pass them as standalone.
projects: Object.fromEntries(
Object.entries(workspace.projects).map(([project, config]) => [
project,
config.root,
])
),
});
}
// no match, default to existing behavior
if (!workspaceJsonOverride && !nxJsonChange) {
return super.read(path);
}
// we try to format it, if it changes, return it, otherwise return the original change
try {
return this.__readWorkspaceConfiguration(actualConfigFileName, {
// we are overriding workspaceJson + nxJson,
workspace: workspaceJsonOverride,
nx: nxJsonChange
? of(parseJson(nxJsonChange.content.toString()))
: null,
});
} catch (e) {
return super.read(path);
}
} else {
const match = findMatchingFileChange(this.host, path);
if (match) {
// found a matching change in the host
return of(Buffer.from(match.content));
} else if (
// found a change to workspace config, and reading a project config file
basename(path) === 'project.json' &&
findWorkspaceConfigFileChange(this.host)
) {
return of(this.host.read(path));
} else {
// found neither, use default read method
return 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);
}
}
type WorkspaceConfigFileChange = FileChange & {
path: ConfigFilePath;
};
function findWorkspaceConfigFileChange(host: Tree): WorkspaceConfigFileChange {
return host
.listChanges()
.find(
(f) => f.path == 'workspace.json' || f.path == 'angular.json'
) as WorkspaceConfigFileChange;
}
function findCreatedProjects(host: Tree): FileChange[] {
return host
.listChanges()
.filter(
(f) =>
f.type === 'CREATE' &&
(basename(f.path) === 'project.json' ||
basename(f.path) === 'package.json')
);
}
function findDeletedProjects(host: Tree): FileChange[] {
return host
.listChanges()
.filter((f) => f.type === 'DELETE' && basename(f.path) === 'project.json');
}
function findMatchingFileChange(host: Tree, path: Path) {
const targetPath = normalize(
path.startsWith('/') ? path.substring(1) : path.toString()
);
return host
.listChanges()
.find((f) => f.type !== 'DELETE' && normalize(f.path) === targetPath);
}
function isWorkspaceConfigPath(p: Path | string) {
return (
p === 'angular.json' ||
p === '/angular.json' ||
p === 'workspace.json' ||
p === '/workspace.json'
);
}
export async function generate(
root: string,
opts: GenerateOptions,
verbose: boolean
) {
const logger = getLogger(verbose);
const fsHost = new NxScopedHost(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;
}
function createPromptProvider() {
interface Prompt {
name: string;
type: 'input' | 'select' | 'multiselect' | 'confirm' | 'numeral';
message: string;
initial?: any;
choices?: (string | { name: string; message: string })[];
validate?: (value: string) => boolean | string;
}
return (definitions: Array<any>) => {
const questions: Prompt[] = definitions.map((definition) => {
const question: Prompt = {
name: definition.id,
message: definition.message,
} as any;
if (definition.default) {
question.initial = definition.default;
}
const validator = definition.validator;
if (validator) {
question.validate = (input) => validator(input);
}
switch (definition.type) {
case 'string':
case 'input':
return { ...question, type: 'input' };
case 'boolean':
case 'confirmation':
case 'confirm':
return { ...question, type: 'confirm' };
case 'number':
case 'numeral':
return { ...question, type: 'numeral' };
case 'list':
return {
...question,
type: !!definition.multiselect ? 'multiselect' : 'select',
choices:
definition.items &&
definition.items.map((item) => {
if (typeof item == 'string') {
return item;
} else {
return {
message: item.label,
name: item.value,
};
}
}),
};
default:
return { ...question, type: definition.type };
}
});
return require('enquirer').prompt(questions);
};
}
export async function runMigration(
root: string,
packageName: string,
migrationName: string,
isVerbose: boolean
) {
const logger = getLogger(isVerbose);
const fsHost = new NxScopedHost(root);
const workflow = createWorkflow(fsHost, root, {});
const collection = resolveMigrationsCollection(packageName);
return workflow
.execute({
collection,
schematic: migrationName,
options: {},
debug: false,
logger: logger as any,
})
.toPromise();
}
function resolveMigrationsCollection(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}"`);
}
function convertEventTypeToHandleMultipleConfigNames(
host: Tree,
eventPath: string,
content: Buffer | never
) {
const actualConfigName = host.exists('/angular.json')
? 'angular.json'
: 'workspace.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/generators.json'),
* '@nrwl/angular': path.join(__dirname, '../../../../angular/generators.json'),
* '@nrwl/linter': path.join(__dirname, '../../../../linter/generators.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.slice(1)
: event.path;
const r = convertEventTypeToHandleMultipleConfigNames(
host,
eventPath,
(event as any).content
);
if (event.kind === 'error') {
} else if (event.kind === 'update') {
if (
r.eventPath === 'angular.json' ||
r.eventPath === 'workspace.json'
) {
saveWorkspaceConfigurationInWrappedSchematic(host, r);
} else {
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(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(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.slice(3)))}\n`;
}
return chalk.bold(chalk.red(s));
},
info: (s) => {
if (s.startsWith('NX ')) {
return `\n${NX_PREFIX} ${chalk.bold(s.slice(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/packages/angular/generators/convert-tslint-to-eslint.';
}
return chalk.bold(chalk.yellow(s));
},
}
);
return tslintExecutorLogger;
};
function saveWorkspaceConfigurationInWrappedSchematic(
host: Tree,
r: { eventPath: string; content: Buffer }
) {
const workspaceJsonExists = host.exists(r.eventPath);
const workspace: Omit<AngularJsonConfiguration, 'projects'> & {
projects: {
[key: string]: string | { configFilePath?: string; root: string };
};
} = parseJson(r.content.toString());
for (const [project, config] of Object.entries(workspace.projects)) {
if (
typeof config === 'object' &&
(!workspaceJsonExists || config.configFilePath)
) {
const path = config.configFilePath || join(config.root, 'project.json');
workspace.projects[project] = normalize(dirname(path));
delete config.root; // remove the root before writing
delete config.configFilePath;
host.write(path, serializeJson(config));
}
}
const nxJson: NxJsonConfiguration = parseJson(
host.read('nx.json').toString()
);
nxJson.generators = workspace.generators || workspace.schematics;
nxJson.cli = workspace.cli || nxJson.cli;
nxJson.defaultProject = workspace.defaultProject;
delete workspace.cli;
delete workspace.generators;
delete workspace.schematics;
if (workspaceJsonExists) {
r.content = Buffer.from(serializeJson(workspace));
host.write(r.eventPath, r.content);
}
}