Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(schematic-utils): schematic utils from nx devkit #15

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
445 changes: 445 additions & 0 deletions packages/schematic-utils/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/schematic-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"preversion": "npm run build"
},
"dependencies": {
"@nrwl/cli": "^12.5.6",
"source-map": "^0.7.3"
},
"devDependencies": {
"@types/node": "^12.12.70"
}
}
255 changes: 255 additions & 0 deletions packages/schematic-utils/src/engine/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { analytics, logging } from "@angular-devkit/core";
import { Observable } from "rxjs";
import { Url } from "url";
import { FileEntry, MergeStrategy, Tree } from "../tree/interface";
import { Workflow } from "../workflow/interface";

export interface TaskConfiguration<T = any> {
name: string;
dependencies?: Array<TaskId>;
options?: T;
}

export interface TaskConfigurationGenerator<T = any> {
toConfiguration(): TaskConfiguration<T>;
}

export type TaskExecutor<T = any> = (
options: T | undefined,
context: SchematicContext
) => Promise<void> | Observable<void>;

export interface TaskExecutorFactory<T> {
readonly name: string;
create(options?: T): Promise<TaskExecutor> | Observable<TaskExecutor>;
}

export interface TaskId {
readonly id: number;
}

export interface TaskInfo {
readonly id: number;
readonly priority: number;
readonly configuration: TaskConfiguration;
readonly context: SchematicContext;
}

export interface ExecutionOptions {
scope: string;
interactive: boolean;
}

/**
* The description (metadata) of a collection. This type contains every information the engine
* needs to run. The CollectionMetadataT type parameter contains additional metadata that you
* want to store while remaining type-safe.
*/
export type CollectionDescription<CollectionMetadataT> = CollectionMetadataT & {
readonly name: string;
readonly extends?: string[];
};

/**
* The description (metadata) of a schematic. This type contains every information the engine
* needs to run. The SchematicMetadataT and CollectionMetadataT type parameters contain additional
* metadata that you want to store while remaining type-safe.
*/
export type SchematicDescription<
CollectionMetadataT,
SchematicMetadataT
> = SchematicMetadataT & {
readonly collection: CollectionDescription<CollectionMetadataT>;
readonly name: string;
readonly private?: boolean;
readonly hidden?: boolean;
};

/**
* The Host for the Engine. Specifically, the piece of the tooling responsible for resolving
* collections and schematics descriptions. The SchematicMetadataT and CollectionMetadataT type
* parameters contain additional metadata that you want to store while remaining type-safe.
*/
export interface EngineHost<
CollectionMetadataT extends any,
SchematicMetadataT extends any
> {
createCollectionDescription(
name: string,
requester?: CollectionDescription<CollectionMetadataT>
): CollectionDescription<CollectionMetadataT>;
listSchematicNames(
collection: CollectionDescription<CollectionMetadataT>
): string[];

createSchematicDescription(
name: string,
collection: CollectionDescription<CollectionMetadataT>
): SchematicDescription<CollectionMetadataT, SchematicMetadataT> | null;
getSchematicRuleFactory<OptionT extends any>(
schematic: SchematicDescription<CollectionMetadataT, SchematicMetadataT>,
collection: CollectionDescription<CollectionMetadataT>
): RuleFactory<OptionT>;
createSourceFromUrl(
url: Url,
context: TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
): Source | null;
transformOptions<OptionT, ResultT>(
schematic: SchematicDescription<CollectionMetadataT, SchematicMetadataT>,
options: OptionT,
context?: TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
): Observable<ResultT>;
transformContext(
context: TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
): TypedSchematicContext<CollectionMetadataT, SchematicMetadataT> | void;
createTaskExecutor(name: string): Observable<TaskExecutor>;
hasTaskExecutor(name: string): boolean;

readonly defaultMergeStrategy?: MergeStrategy;
}

/**
* The root Engine for creating and running schematics and collections. Everything related to
* a schematic execution starts from this interface.ts.
*
* CollectionMetadataT is, by default, a generic Collection metadata type. This is used throughout
* the engine typings so that you can use a type that's merged into descriptions, while being
* type-safe.
*
* SchematicMetadataT is a type that contains additional typing for the Schematic Description.
*/
export interface Engine<
CollectionMetadataT extends any,
SchematicMetadataT extends any
> {
createCollection(
name: string,
requester?: Collection<CollectionMetadataT, SchematicMetadataT>
): Collection<CollectionMetadataT, SchematicMetadataT>;
createContext(
schematic: Schematic<CollectionMetadataT, SchematicMetadataT>,
parent?: Partial<
TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
>,
executionOptions?: Partial<ExecutionOptions>
): TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>;
createSchematic(
name: string,
collection: Collection<CollectionMetadataT, SchematicMetadataT>
): Schematic<CollectionMetadataT, SchematicMetadataT>;
createSourceFromUrl(
url: Url,
context: TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
): Source;
transformOptions<OptionT extends any, ResultT extends any>(
schematic: Schematic<CollectionMetadataT, SchematicMetadataT>,
options: OptionT,
context?: TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
): Observable<ResultT>;
executePostTasks(): Observable<void>;

readonly defaultMergeStrategy: MergeStrategy;
readonly workflow: Workflow | null;
}

/**
* A Collection as created by the Engine. This should be used by the tool to create schematics,
* or by rules to create other schematics as well.
*/
export interface Collection<
CollectionMetadataT extends any,
SchematicMetadataT extends any
> {
readonly description: CollectionDescription<CollectionMetadataT>;
readonly baseDescriptions?: Array<CollectionDescription<CollectionMetadataT>>;

createSchematic(
name: string,
allowPrivate?: boolean
): Schematic<CollectionMetadataT, SchematicMetadataT>;
listSchematicNames(): string[];
}

/**
* A Schematic as created by the Engine. This should be used by the tool to execute the main
* schematics, or by rules to execute other schematics as well.
*/
export interface Schematic<
CollectionMetadataT extends any,
SchematicMetadataT extends any
> {
readonly description: SchematicDescription<
CollectionMetadataT,
SchematicMetadataT
>;
readonly collection: Collection<CollectionMetadataT, SchematicMetadataT>;

call<OptionT extends any>(
options: OptionT,
host: Observable<Tree>,
parentContext?: Partial<
TypedSchematicContext<CollectionMetadataT, SchematicMetadataT>
>,
executionOptions?: Partial<ExecutionOptions>
): Observable<Tree>;
}

/**
* A SchematicContext. Contains information necessary for Schematics to execute some rules, for
* example when using another schematics, as we need the engine and collection.
*/
export interface TypedSchematicContext<
CollectionMetadataT extends any,
SchematicMetadataT extends any
> {
readonly debug: boolean;
readonly engine: Engine<CollectionMetadataT, SchematicMetadataT>;
readonly logger: logging.LoggerApi;
readonly schematic: Schematic<CollectionMetadataT, SchematicMetadataT>;
readonly strategy: MergeStrategy;
readonly interactive: boolean;
addTask<T>(
task: TaskConfigurationGenerator<T>,
dependencies?: Array<TaskId>
): TaskId;

// This might be undefined if the feature is unsupported.
/** @deprecated since version 11 - as it's unused. */
readonly analytics?: analytics.Analytics;
}

/**
* This is used by the Schematics implementations in order to avoid needing to have typing from
* the tooling. Schematics are not specific to a tool.
*/
export type SchematicContext = TypedSchematicContext<any, any>;

/**
* A rule factory, which is normally the way schematics are implemented. Returned by the tooling
* after loading a schematic description.
*/
export type RuleFactory<T extends any> = (options: T) => Rule;

/**
* A FileOperator applies changes synchronously to a FileEntry. An async operator returns
* asynchronously. We separate them so that the type system can catch early errors.
*/
export type FileOperator = (entry: FileEntry) => FileEntry | null;
export type AsyncFileOperator = (
tree: FileEntry
) => Observable<FileEntry | null>;

/**
* A source is a function that generates a Tree from a specific context. A rule transforms a tree
* into another tree from a specific context. In both cases, an Observable can be returned if
* the source or the rule are asynchronous. Only the last Tree generated in the observable will
* be used though.
*
* We obfuscate the context of Source and Rule because the schematic implementation should not
* know which types is the schematic or collection metadata, as they are both tooling specific.
*/
export type Source = (context: SchematicContext) => Tree | Observable<Tree>;
export type Rule = (
tree: Tree,
context: SchematicContext
) => Tree | Observable<Tree> | Rule | Promise<void | Rule> | void;
7 changes: 7 additions & 0 deletions packages/schematic-utils/src/exceptions/exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,10 @@ export class UnsupportedPlatformException extends BaseException {
super("This platform is not supported by this code path.");
}
}
export class UnsuccessfulWorkflowExecution extends BaseException {
constructor() {
super("Workflow did not execute successfully.");
}
}

export class SchematicsException extends BaseException {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Tree } from "@nrwl/tao/src/shared/tree";
import { visitNotIgnoredFiles } from "./visit-not-ignored-files";
import { createTree } from "../tests/create-tree";

describe("visitNotIgnoredFiles", () => {
let tree: Tree;

beforeEach(() => {
tree = createTree();
});

it("should visit files recursively in a directory", () => {
tree.write("dir/file1.ts", "");
tree.write("dir/dir2/file2.ts", "");

const visitor = jest.fn();
visitNotIgnoredFiles(tree, "dir", visitor);

expect(visitor).toHaveBeenCalledWith("dir/file1.ts");
expect(visitor).toHaveBeenCalledWith("dir/dir2/file2.ts");
});

it("should not visit ignored files in a directory", () => {
tree.write(".gitignore", "node_modules");

tree.write("dir/file1.ts", "");
tree.write("dir/node_modules/file1.ts", "");
tree.write("dir/dir2/file2.ts", "");

const visitor = jest.fn();
visitNotIgnoredFiles(tree, "dir", visitor);

expect(visitor).toHaveBeenCalledWith("dir/file1.ts");
expect(visitor).toHaveBeenCalledWith("dir/dir2/file2.ts");
expect(visitor).not.toHaveBeenCalledWith("dir/node_modules/file1.ts");
});

it("should be able to visit the root", () => {
tree.write(".gitignore", "node_modules");

tree.write("dir/file1.ts", "");
tree.write("dir/node_modules/file1.ts", "");
tree.write("dir/dir2/file2.ts", "");

const visitor = jest.fn();
visitNotIgnoredFiles(tree, "", visitor);

expect(visitor).toHaveBeenCalledWith(".gitignore");
expect(visitor).toHaveBeenCalledWith("dir/file1.ts");
expect(visitor).toHaveBeenCalledWith("dir/dir2/file2.ts");
expect(visitor).not.toHaveBeenCalledWith("dir/node_modules/file1.ts");
});
});
35 changes: 35 additions & 0 deletions packages/schematic-utils/src/generators/visit-not-ignored-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Tree } from "@nrwl/tao/src/shared/tree";
import { join } from "path";
import ignore, { Ignore } from "ignore";

/**
* Utility to act on all files in a tree that are not ignored by git.
*/
export function visitNotIgnoredFiles(
tree: Tree,
dirPath: string = tree.root,
visitor: (path: string) => void
): void {
let ig: Ignore;
if (tree.exists(".gitignore")) {
ig = ignore();
// @ts-ignore
ig.add(tree.read(".gitignore", "utf-8"));
}
// @ts-ignore
if (dirPath !== "" && ig?.ignores(dirPath)) {
return;
}
for (const child of tree.children(dirPath)) {
const fullPath = join(dirPath, child);
// @ts-ignore
if (ig?.ignores(fullPath)) {
continue;
}
if (tree.isFile(fullPath)) {
visitor(fullPath);
} else {
visitNotIgnoredFiles(tree, fullPath, visitor);
}
}
}
Loading