From 0fffc6d003651c17c60c6c3e0814990822a63ee0 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Tue, 27 Oct 2026 18:38:38 +0530 Subject: [PATCH 1/3] fix(@angular-devkit/schematics): support delegating trees in the `filter` rule `filter()` only accepted a `HostTree` and threw "Tree type is not supported." for any other tree. A `UnitTestTree` is a `DelegateTree` around a `HostTree`, so a schematic that uses `filter()` failed in unit tests when run on the tree returned by a previous `runSchematic` call. Unwrap delegating trees and filter the `HostTree` they wrap. Closes #26000 --- .../schematics/src/rules/base.ts | 12 +++++- .../schematics/src/rules/base_spec.ts | 37 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/schematics/src/rules/base.ts b/packages/angular_devkit/schematics/src/rules/base.ts index 1f5d3023c28e..7ead6110510b 100644 --- a/packages/angular_devkit/schematics/src/rules/base.ts +++ b/packages/angular_devkit/schematics/src/rules/base.ts @@ -9,6 +9,7 @@ import { concat, lastValueFrom, map, mapTo, toArray } from 'rxjs'; import { FileOperator, Rule, Source } from '../engine/interface'; import { SchematicsException } from '../exception/exception'; +import { DelegateTree } from '../tree/delegate'; import { FilterHostTree, HostTree } from '../tree/host-tree'; import { FileEntry, FilePredicate, MergeStrategy, Tree } from '../tree/interface'; import { ScopedTree } from '../tree/scoped'; @@ -74,8 +75,15 @@ export function noop(): Rule { export function filter(predicate: FilePredicate): Rule { return (tree: Tree) => { - if (HostTree.isHostTree(tree)) { - return new FilterHostTree(tree, predicate); + // Trees such as `UnitTestTree` delegate to a `HostTree`, so filter the tree they wrap. + let hostTree = tree; + while (hostTree instanceof DelegateTree) { + // cast to allow access + hostTree = (hostTree as unknown as { _other: Tree })._other; + } + + if (HostTree.isHostTree(hostTree)) { + return new FilterHostTree(hostTree, predicate); } else { throw new SchematicsException('Tree type is not supported.'); } diff --git a/packages/angular_devkit/schematics/src/rules/base_spec.ts b/packages/angular_devkit/schematics/src/rules/base_spec.ts index 6eb299ef9890..fab750d5703f 100644 --- a/packages/angular_devkit/schematics/src/rules/base_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/base_spec.ts @@ -8,11 +8,12 @@ import { Path, virtualFs } from '@angular-devkit/core'; import { lastValueFrom, of as observableOf } from 'rxjs'; +import { UnitTestTree } from '../../testing'; import { Rule, SchematicContext, Source } from '../engine/interface'; import { HostTree } from '../tree/host-tree'; import { MergeStrategy, Tree } from '../tree/interface'; import { empty } from '../tree/static'; -import { apply, applyToSubtree, chain, partitionApplyMerge } from './base'; +import { apply, applyToSubtree, chain, filter, partitionApplyMerge } from './base'; import { callRule, callSource } from './call'; import { move } from './move'; @@ -242,3 +243,37 @@ describe('applyToSubtree', () => { expect(result.exists('a/c/file3')).toBe(true); }); }); + +describe('filter', () => { + it('works with a HostTree', async () => { + const tree = new HostTree(); + tree.create('/file1.ts', ''); + tree.create('/file2.js', ''); + + const result = await lastValueFrom( + callRule( + filter((path) => path.endsWith('.ts')), + observableOf(tree), + context, + ), + ); + expect(result.exists('/file1.ts')).toBe(true); + expect(result.exists('/file2.js')).toBe(false); + }); + + it('works with a UnitTestTree', async () => { + const tree = new HostTree(); + tree.create('/file1.ts', ''); + tree.create('/file2.js', ''); + + const result = await lastValueFrom( + callRule( + filter((path) => path.endsWith('.ts')), + observableOf(new UnitTestTree(new UnitTestTree(tree))), + context, + ), + ); + expect(result.exists('/file1.ts')).toBe(true); + expect(result.exists('/file2.js')).toBe(false); + }); +}); From 1f1d28d681eebe52187718adcec609fd79a5a9d9 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Tue, 27 Oct 2026 21:19:23 +0530 Subject: [PATCH 2/3] fixup! fix(@angular-devkit/schematics): support delegating trees in the `filter` rule --- packages/angular_devkit/schematics/src/rules/base.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/angular_devkit/schematics/src/rules/base.ts b/packages/angular_devkit/schematics/src/rules/base.ts index 7ead6110510b..7c85e5678cbd 100644 --- a/packages/angular_devkit/schematics/src/rules/base.ts +++ b/packages/angular_devkit/schematics/src/rules/base.ts @@ -75,15 +75,12 @@ export function noop(): Rule { export function filter(predicate: FilePredicate): Rule { return (tree: Tree) => { - // Trees such as `UnitTestTree` delegate to a `HostTree`, so filter the tree they wrap. - let hostTree = tree; - while (hostTree instanceof DelegateTree) { - // cast to allow access - hostTree = (hostTree as unknown as { _other: Tree })._other; + if (tree instanceof DelegateTree) { + tree = tree.branch(); } - if (HostTree.isHostTree(hostTree)) { - return new FilterHostTree(hostTree, predicate); + if (HostTree.isHostTree(tree)) { + return new FilterHostTree(tree, predicate); } else { throw new SchematicsException('Tree type is not supported.'); } From 68b5efae4f70d7beedf55d5e2651b8a0ed8fcbd8 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Tue, 27 Oct 2026 21:21:33 +0530 Subject: [PATCH 3/3] fix(@angular-devkit/schematics): support delegating trees in `partition` `partition()`, which backs the `partitionApplyMerge` rule, threw "Tree type is not supported." for a `UnitTestTree` and other delegating trees. Branch the tree they wrap instead, as `filter()` does. --- .../schematics/src/rules/base_spec.ts | 26 +++++++++++++++++++ .../schematics/src/tree/static.ts | 5 ++++ 2 files changed, 31 insertions(+) diff --git a/packages/angular_devkit/schematics/src/rules/base_spec.ts b/packages/angular_devkit/schematics/src/rules/base_spec.ts index fab750d5703f..94bbd228ce49 100644 --- a/packages/angular_devkit/schematics/src/rules/base_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/base_spec.ts @@ -226,6 +226,32 @@ describe('partitionApplyMerge', () => { expect(result.exists('/test1')).toBe(false); expect(result.exists('/test2')).toBe(false); }); + + it('works with a UnitTestTree', async () => { + const tree = new HostTree(); + tree.create('/test1', ''); + tree.create('/test2', ''); + const predicate = (path: Path) => path.indexOf('1') != -1; + + const ruleYes: Rule = (tree: Tree) => { + expect(tree.exists('/test1')).toBe(true); + expect(tree.exists('/test2')).toBe(false); + }; + const ruleNo: Rule = (tree: Tree) => { + expect(tree.exists('/test1')).toBe(false); + expect(tree.exists('/test2')).toBe(true); + }; + + const result = await lastValueFrom( + callRule( + partitionApplyMerge(predicate, ruleYes, ruleNo), + observableOf(new UnitTestTree(new UnitTestTree(tree))), + context, + ), + ); + expect(result.exists('/test1')).toBe(true); + expect(result.exists('/test2')).toBe(true); + }); }); describe('applyToSubtree', () => { diff --git a/packages/angular_devkit/schematics/src/tree/static.ts b/packages/angular_devkit/schematics/src/tree/static.ts index f4a7ce1b8618..e28520128827 100644 --- a/packages/angular_devkit/schematics/src/tree/static.ts +++ b/packages/angular_devkit/schematics/src/tree/static.ts @@ -7,6 +7,7 @@ */ import { SchematicsException } from '../exception/exception'; +import { DelegateTree } from './delegate'; import { FilterHostTree, HostTree } from './host-tree'; import { FilePredicate, MergeStrategy, Tree } from './interface'; @@ -29,6 +30,10 @@ export function merge( } export function partition(tree: Tree, predicate: FilePredicate): [Tree, Tree] { + if (tree instanceof DelegateTree) { + tree = tree.branch(); + } + if (tree instanceof HostTree) { return [ new FilterHostTree(tree, predicate),