nx/packages/devkit/src/utils/invoke-nx-generator.ts
2024-05-01 12:12:32 -04:00

217 lines
5.5 KiB
TypeScript

import { join, relative } from 'path';
import type { Mode } from 'fs';
import type { TreeWriteOptions } from 'nx/src/generators/tree';
import {
FileChange,
Generator,
GeneratorCallback,
logger,
Tree,
} from 'nx/src/devkit-exports';
import { stripIndent } from 'nx/src/devkit-internals';
class RunCallbackTask {
constructor(private callback: GeneratorCallback) {}
toConfiguration() {
return {
name: 'RunCallback',
options: {
callback: this.callback,
},
};
}
}
function createRunCallbackTask() {
return {
name: 'RunCallback',
create: () => {
return Promise.resolve(
async ({ callback }: { callback: GeneratorCallback }) => {
await callback();
}
);
},
};
}
/**
* Convert an Nx Generator into an Angular Devkit Schematic.
* @param generator The Nx generator to convert to an Angular Devkit Schematic.
*/
export function convertNxGenerator<T = any>(
generator: Generator<T>,
skipWritingConfigInOldFormat: boolean = false
) {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
return (generatorOptions: T) =>
invokeNxGenerator(generator, generatorOptions);
}
/**
* Create a Rule to invoke an Nx Generator
*/
function invokeNxGenerator<T = any>(
generator: Generator<T>,
options: T,
skipWritingConfigInOldFormat?: boolean
) {
return async (tree, context) => {
if (context.engine.workflow) {
const engineHost = (context.engine.workflow as any).engineHost;
engineHost.registerTaskExecutor(createRunCallbackTask());
}
const root =
context.engine.workflow && context.engine.workflow.engineHost.paths
? context.engine.workflow.engineHost.paths[1]
: tree.root.path;
const adapterTree = new DevkitTreeFromAngularDevkitTree(
tree,
root,
skipWritingConfigInOldFormat
);
const result = await generator(adapterTree, options);
if (!result) {
return adapterTree['tree'];
}
if (typeof result === 'function') {
if (context.engine.workflow) {
context.addTask(new RunCallbackTask(result));
}
}
};
}
const actionToFileChangeMap = {
c: 'CREATE',
o: 'UPDATE',
d: 'DELETE',
};
class DevkitTreeFromAngularDevkitTree implements Tree {
private configFileName: string;
constructor(
private tree,
private _root: string,
private skipWritingConfigInOldFormat?: boolean
) {
/**
* When using the UnitTestTree from @angular-devkit/schematics/testing, the root is just `/`.
* This causes a massive issue if `getProjects()` is used in the underlying generator because it
* causes fast-glob to be set to work on the user's entire file system.
*
* Therefore, in this case, patch the root to match what Nx Devkit does and use /virtual instead.
*/
try {
const { UnitTestTree } = require('@angular-devkit/schematics/testing');
if (tree instanceof UnitTestTree && _root === '/') {
this._root = '/virtual';
}
} catch {}
}
get root(): string {
return this._root;
}
children(dirPath: string): string[] {
const { subdirs, subfiles } = this.tree.getDir(dirPath);
return [...subdirs, ...subfiles];
}
delete(filePath: string): void {
this.tree.delete(filePath);
}
exists(filePath: string): boolean {
if (this.isFile(filePath)) {
return this.tree.exists(filePath);
} else {
return this.children(filePath).length > 0;
}
}
isFile(filePath: string): boolean {
return this.tree.exists(filePath) && !!this.tree.read(filePath);
}
listChanges(): FileChange[] {
const fileChanges = [];
for (const action of this.tree.actions) {
if (action.kind === 'r') {
fileChanges.push({
path: this.normalize(action.to),
type: 'CREATE',
content: this.read(action.to),
});
fileChanges.push({
path: this.normalize(action.path),
type: 'DELETE',
content: null,
});
} else if (action.kind === 'c' || action.kind === 'o') {
fileChanges.push({
path: this.normalize(action.path),
type: actionToFileChangeMap[action.kind],
content: action.content,
});
} else {
fileChanges.push({
path: this.normalize(action.path),
type: 'DELETE',
content: null,
});
}
}
return fileChanges;
}
private normalize(path: string): string {
return relative(this.root, join(this.root, path));
}
read(filePath: string): Buffer;
read(filePath: string, encoding: BufferEncoding): string;
read(filePath: string, encoding?: BufferEncoding) {
return encoding
? this.tree.read(filePath).toString(encoding)
: this.tree.read(filePath);
}
rename(from: string, to: string): void {
this.tree.rename(from, to);
}
write(
filePath: string,
content: Buffer | string,
options?: TreeWriteOptions
): void {
if (options?.mode) {
this.warnUnsupportedFilePermissionsChange(filePath, options.mode);
}
if (this.tree.exists(filePath)) {
this.tree.overwrite(filePath, content);
} else {
this.tree.create(filePath, content);
}
}
changePermissions(filePath: string, mode: Mode): void {
this.warnUnsupportedFilePermissionsChange(filePath, mode);
}
private warnUnsupportedFilePermissionsChange(filePath: string, mode: Mode) {
logger.warn(
stripIndent(`The Angular DevKit tree does not support changing a file permissions.
Ignoring changing ${filePath} permissions to ${mode}.`)
);
}
}