From 9c9502b012159649241ef78d2d3ae967b885960e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Turbi=C5=84ski?= Date: Tue, 13 Oct 2026 20:01:06 +0000 Subject: [PATCH] fix(@angular-devkit/schematics): avoid stack overflow when chaining many rules The chain rule nested one observable per rule, so subscribing to a chain of a few thousand rules exceeded the call stack and the result never settled. Each rule result is now awaited before the next rule runs. --- .../angular_devkit/schematics/src/rules/base.ts | 10 +++++----- .../schematics/src/rules/base_spec.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/angular_devkit/schematics/src/rules/base.ts b/packages/angular_devkit/schematics/src/rules/base.ts index ec380f2aeb0d..1f5d3023c28e 100644 --- a/packages/angular_devkit/schematics/src/rules/base.ts +++ b/packages/angular_devkit/schematics/src/rules/base.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Observable, concat, map, mapTo, toArray } from 'rxjs'; +import { concat, lastValueFrom, map, mapTo, toArray } from 'rxjs'; import { FileOperator, Rule, Source } from '../engine/interface'; import { SchematicsException } from '../exception/exception'; import { FilterHostTree, HostTree } from '../tree/host-tree'; @@ -34,18 +34,18 @@ export function empty(): Source { */ export function chain(rules: Iterable | AsyncIterable): Rule { return async (initialTree, context) => { - let intermediateTree: Observable | undefined; + let intermediateTree = initialTree; if (Symbol.asyncIterator in rules) { for await (const rule of rules) { - intermediateTree = callRule(rule, intermediateTree ?? initialTree, context); + intermediateTree = await lastValueFrom(callRule(rule, intermediateTree, context)); } } else { for (const rule of rules) { - intermediateTree = callRule(rule, intermediateTree ?? initialTree, context); + intermediateTree = await lastValueFrom(callRule(rule, intermediateTree, context)); } } - return () => intermediateTree; + return intermediateTree; }; } diff --git a/packages/angular_devkit/schematics/src/rules/base_spec.ts b/packages/angular_devkit/schematics/src/rules/base_spec.ts index b3578ca8c00a..6eb299ef9890 100644 --- a/packages/angular_devkit/schematics/src/rules/base_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/base_spec.ts @@ -140,6 +140,19 @@ describe('chain', () => { expect(rulesCalled[2]).toBe(tree2); expect(result).toBe(tree3); }); + + it('works with a large number of rules', async () => { + const tree0 = empty(); + let callCount = 0; + const rules: Rule[] = Array.from({ length: 20_000 }, () => () => { + callCount++; + }); + + const result = await lastValueFrom(callRule(chain(rules), tree0, context)); + + expect(result).toBe(tree0); + expect(callCount).toBe(20_000); + }); }); describe('apply', () => {