From 8bc31a515ff6e8edda6ea5786a47ae5a788acd36 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Thu, 19 Mar 2026 01:49:21 +0100 Subject: [PATCH 01/15] feat(core): Allow other expression for exhaustive typechecking When the switched expression is nested within a union, exhaustive typechecking needs to know which expression to check. This change adds the possibility of specifying the expression to check: ``` @Component({ selector: 'app-root', imports: [], template: ` @switch (state.mode) { @case ('show') { {{ state.menu }}; } @case ('hide') {} @default never(state); } `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class App { state!: { mode: 'hide' } | { mode: 'show'; menu: number };; } ``` fixes #67406 --- .../content/guide/templates/control-flow.md | 17 +++++++ .../ngtsc/typecheck/src/ops/switch_block.ts | 7 ++- .../ngtsc/typecheck/test/diagnostics_spec.ts | 19 ++++++++ .../typecheck/test/type_check_block_spec.ts | 21 +++++++++ packages/compiler/src/ml_parser/lexer.ts | 16 +++---- packages/compiler/src/render3/r3_ast.ts | 1 + .../compiler/src/render3/r3_control_flow.ts | 5 +++ .../compiler/src/render3/view/t2_binder.ts | 2 +- .../compiler/test/ml_parser/lexer_spec.ts | 10 +++++ .../language-service/src/template_target.ts | 4 +- tools/manual_api_docs/blocks/switch.md | 17 +++++++ .../syntaxes/test/data/template-blocks.html | 5 +++ .../test/data/template-blocks.html.snap | 45 ++++++++++++++----- 13 files changed, 147 insertions(+), 22 deletions(-) diff --git a/adev/src/content/guide/templates/control-flow.md b/adev/src/content/guide/templates/control-flow.md index 6d812254739f..cb88c26490ab 100644 --- a/adev/src/content/guide/templates/control-flow.md +++ b/adev/src/content/guide/templates/control-flow.md @@ -160,3 +160,20 @@ export class AppComponent { state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut'; } ``` + +When the switched expression is nested within a union, you must explicitly specify the expression to check for exhaustiveness. + +```angular-ts +@Component({ + template: ` + @switch (state.mode) { + @case ('show') { {{ state.menu }}; } + @case ('hide') {} + @default never(state); + } + `, +}) +export class App { + state!: {mode: 'hide'} | {mode: 'show'; menu: number}; +} +``` diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/switch_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/switch_block.ts index 7798a78efde5..2b9cad1e29af 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/switch_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/switch_block.ts @@ -60,7 +60,12 @@ export class TcbSwitchOp extends TcbOp { }); if (this.block.exhaustiveCheck) { - const switchValue = tcbExpression(this.block.expression, this.tcb, this.scope); + let translateExpression = this.block.expression; + if (this.block.exhaustiveCheck.expression) { + translateExpression = this.block.exhaustiveCheck.expression; + } + + const switchValue = tcbExpression(translateExpression, this.tcb, this.scope); const exhaustiveId = this.tcb.allocateId(); clauses.push( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts index 21a8ec3a3725..55436d5b4f31 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts @@ -1453,6 +1453,25 @@ class TestComponent { expect(messages).toEqual([]); }); + + it('should narrow union when switching on a nested prop', () => { + const messages = diagnose( + ` + @switch (state.mode) { + @case ('show') { {{ state.menu }}; } + @case ('hide') {} + @default never(state); + } + `, + ` + export class TestComponent { + state: { mode: 'hide' } | { mode: 'show'; menu: number }; + } + `, + ); + + expect(messages).toEqual([]); + }); }); // https://github.com/angular/angular/issues/43970 diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 69c27d136062..e61f06454d31 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -2202,6 +2202,27 @@ describe('type check blocks', () => { ); }); + it('should generate a switch block with exhaustiveness checking with param', () => { + const TEMPLATE = ` + @switch (expr) { + @case (1) { + {{one()}} + } + @case (2) { + {{two()}} + } + @default never(expr.prop); + } + `; + + expect(tcb(TEMPLATE)).toContain( + 'switch (((this).expr)) { ' + + 'case 1: "" + ((this).one()); break; ' + + 'case 2: "" + ((this).two()); break; ' + + 'default: const tcbExhaustive_t1: never = ((((this).expr)).prop);', + ); + }); + it('should not report unused locals for exhaustiveness check variable', () => { const TEMPLATE = ` @switch (expr) { diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index a1553abae325..c463bc4a42ae 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/packages/compiler/src/ml_parser/lexer.ts @@ -299,14 +299,6 @@ class _Tokenizer { this._beginToken(TokenType.BLOCK_OPEN_START, start); const startToken = this._endToken([this._getBlockName()]); - if (startToken.parts[0] === 'default never' && this._attemptCharCode(chars.$SEMICOLON)) { - this._beginToken(TokenType.BLOCK_OPEN_END); - this._endToken([]); - this._beginToken(TokenType.BLOCK_CLOSE); - this._endToken([]); - return; - } - if (this._cursor.peek() === chars.$LPAREN) { // Advance past the opening paren. this._cursor.advance(); @@ -324,6 +316,14 @@ class _Tokenizer { } } + if (startToken.parts[0] === 'default never' && this._attemptCharCode(chars.$SEMICOLON)) { + this._beginToken(TokenType.BLOCK_OPEN_END); + this._endToken([]); + this._beginToken(TokenType.BLOCK_CLOSE); + this._endToken([]); + return; + } + if (this._attemptCharCode(chars.$LBRACE)) { this._beginToken(TokenType.BLOCK_OPEN_END); this._endToken([]); diff --git a/packages/compiler/src/render3/r3_ast.ts b/packages/compiler/src/render3/r3_ast.ts index 61303e61fdbe..bd6cb2988d9b 100644 --- a/packages/compiler/src/render3/r3_ast.ts +++ b/packages/compiler/src/render3/r3_ast.ts @@ -471,6 +471,7 @@ export class SwitchBlockCaseGroup extends BlockNode implements Node { export class SwitchExhaustiveCheck extends BlockNode implements Node { constructor( + public expression: AST | null, sourceSpan: ParseSourceSpan, startSourceSpan: ParseSourceSpan, endSourceSpan: ParseSourceSpan | null, diff --git a/packages/compiler/src/render3/r3_control_flow.ts b/packages/compiler/src/render3/r3_control_flow.ts index 78fe0434da8c..0f37877ffb02 100644 --- a/packages/compiler/src/render3/r3_control_flow.ts +++ b/packages/compiler/src/render3/r3_control_flow.ts @@ -264,6 +264,10 @@ export function createSwitchBlock( if (isCase) { expression = parseBlockParameterToBinding(node.parameters[0], bindingParser); } else if (node.name === 'default never') { + if (node.parameters.length > 0) { + expression = parseBlockParameterToBinding(node.parameters[0], bindingParser); + } + if ( node.children.length > 0 || (node.endSourceSpan !== null && @@ -287,6 +291,7 @@ export function createSwitchBlock( } exhaustiveCheck = new t.SwitchExhaustiveCheck( + expression, node.sourceSpan, node.startSourceSpan, node.endSourceSpan, diff --git a/packages/compiler/src/render3/view/t2_binder.ts b/packages/compiler/src/render3/view/t2_binder.ts index 549ef8a14884..c24ca3fdb79b 100644 --- a/packages/compiler/src/render3/view/t2_binder.ts +++ b/packages/compiler/src/render3/view/t2_binder.ts @@ -953,7 +953,7 @@ class TemplateBinder extends CombinedRecursiveAstVisitor { } override visitSwitchExhaustiveCheck(block: SwitchExhaustiveCheck) { - // There are no bindings/references in the exhaustive check block. + block.expression?.visit(this); } override visitForLoopBlock(block: ForLoopBlock) { diff --git a/packages/compiler/test/ml_parser/lexer_spec.ts b/packages/compiler/test/ml_parser/lexer_spec.ts index e3ebac0e8270..054145ee8f52 100644 --- a/packages/compiler/test/ml_parser/lexer_spec.ts +++ b/packages/compiler/test/ml_parser/lexer_spec.ts @@ -3396,6 +3396,16 @@ describe('HtmlLexer', () => { ]); }); + it('should parse @default never(expr);', () => { + expect(tokenizeAndHumanizeParts('@default never(expr);')).toEqual([ + [TokenType.BLOCK_OPEN_START, 'default never'], + [TokenType.BLOCK_PARAMETER, 'expr'], + [TokenType.BLOCK_OPEN_END], + [TokenType.BLOCK_CLOSE], + [TokenType.EOF], + ]); + }); + it('should parse @default never ;', () => { expect(tokenizeAndHumanizeParts('@default never ;')).toEqual([ [TokenType.BLOCK_OPEN_START, 'default never'], diff --git a/packages/language-service/src/template_target.ts b/packages/language-service/src/template_target.ts index 1fb327e8fa05..360a12a0f1bc 100644 --- a/packages/language-service/src/template_target.ts +++ b/packages/language-service/src/template_target.ts @@ -677,7 +677,9 @@ class TemplateTargetVisitor implements TmplAstVisitor { this.visitAll(block.children); } - visitSwitchExhaustiveCheck(block: TmplAstSwitchExhaustiveCheck) {} + visitSwitchExhaustiveCheck(block: TmplAstSwitchExhaustiveCheck) { + block.expression && this.visitBinding(block.expression); + } visitForLoopBlock(block: TmplAstForLoopBlock) { this.visit(block.item); diff --git a/tools/manual_api_docs/blocks/switch.md b/tools/manual_api_docs/blocks/switch.md index ad742361e74b..a32738f08a28 100644 --- a/tools/manual_api_docs/blocks/switch.md +++ b/tools/manual_api_docs/blocks/switch.md @@ -57,3 +57,20 @@ export class AppComponent { state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut'; } ``` + +When the switched expression is nested within a union, you must explicitly specify the expression to check for exhaustiveness. + +```angular-ts +@Component({ + template: ` + @switch (state.mode) { + @case ('show') { {{ state.menu }}; } + @case ('hide') {} + @default never(state); + } + `, +}) +export class App { + state!: {mode: 'hide'} | {mode: 'show'; menu: number}; +} +``` diff --git a/vscode-ng-language-service/syntaxes/test/data/template-blocks.html b/vscode-ng-language-service/syntaxes/test/data/template-blocks.html index e43a1a5d1af2..628a4b0eb26a 100644 --- a/vscode-ng-language-service/syntaxes/test/data/template-blocks.html +++ b/vscode-ng-language-service/syntaxes/test/data/template-blocks.html @@ -23,6 +23,11 @@ @default never; } +@switch(aOrb) { + @case(a) {} + @default never(aOrb.c); +} + @if (a==b) { hello } @else { goodbye } @if (a==b) { diff --git a/vscode-ng-language-service/syntaxes/test/data/template-blocks.html.snap b/vscode-ng-language-service/syntaxes/test/data/template-blocks.html.snap index bc3c7dc51e1c..0de100e6d501 100644 --- a/vscode-ng-language-service/syntaxes/test/data/template-blocks.html.snap +++ b/vscode-ng-language-service/syntaxes/test/data/template-blocks.html.snap @@ -130,18 +130,41 @@ >} #^ template.blocks.ng control.block.ng punctuation.definition.block.ts > ->@if (a==b) { hello } @else { goodbye } +>@switch(aOrb) { #^ template.blocks.ng control.block.ng keyword.control.block.transition.ng -# ^^ template.blocks.ng control.block.ng keyword.control.block.kind.ng -# ^ template.blocks.ng control.block.ng -# ^ template.blocks.ng control.block.ng meta.brace.round.ts -# ^ template.blocks.ng control.block.ng control.block.expression.ng variable.other.readwrite.ts -# ^^ template.blocks.ng control.block.ng control.block.expression.ng keyword.operator.comparison.ts -# ^ template.blocks.ng control.block.ng control.block.expression.ng variable.other.readwrite.ts -# ^ template.blocks.ng control.block.ng meta.brace.round.ts -# ^ template.blocks.ng control.block.ng -# ^ template.blocks.ng control.block.ng punctuation.definition.block.ts -# ^^^^^^^ template.blocks.ng control.block.ng control.block.body.ng +# ^^^^^^ template.blocks.ng control.block.ng keyword.control.block.kind.ng +# ^ template.blocks.ng control.block.ng meta.brace.round.ts +# ^^^^ template.blocks.ng control.block.ng control.block.expression.ng variable.other.readwrite.ts +# ^ template.blocks.ng control.block.ng meta.brace.round.ts +# ^ template.blocks.ng control.block.ng +# ^ template.blocks.ng control.block.ng punctuation.definition.block.ts +> @case(a) {} +#^^^^ template.blocks.ng control.block.ng control.block.body.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng keyword.control.block.transition.ng +# ^^^^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng keyword.control.block.kind.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng meta.brace.round.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng control.block.expression.ng variable.other.readwrite.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng meta.brace.round.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng punctuation.definition.block.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng punctuation.definition.block.ts +> @default never(aOrb.c); +#^^^^ template.blocks.ng control.block.ng control.block.body.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng keyword.control.block.transition.ng +# ^^^^^^^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng keyword.control.block.kind.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng +# ^^^^^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng meta.brace.round.ts +# ^^^^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng control.block.expression.ng variable.other.object.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng control.block.expression.ng punctuation.accessor.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng control.block.expression.ng variable.other.property.ts +# ^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng meta.brace.round.ts +# ^^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng control.block.case.header.ng +>} +#^ template.blocks.ng control.block.ng control.block.body.ng control.block.case.ng +> +>@if (a==b) { hello } @else { goodbye } +#^^^^^^^^^^^^^^^^^^^ template.blocks.ng control.block.ng control.block.body.ng # ^ template.blocks.ng control.block.ng punctuation.definition.block.ts # ^ template.blocks.ng # ^ template.blocks.ng control.block.ng keyword.control.block.transition.ng From 1782114862cb713fcbf784503eecee4a0902d424 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:54:10 -0500 Subject: [PATCH 02/15] refactor(router): expose `ComponentInputBindingOptions` in public API Expose `ComponentInputBindingOptions` as part of the public API --- goldens/public-api/router/index.api.md | 5 +++++ packages/router/src/index.ts | 1 + packages/router/src/router_config.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/goldens/public-api/router/index.api.md b/goldens/public-api/router/index.api.md index fe21f82e436d..a6e5fc31ef35 100644 --- a/goldens/public-api/router/index.api.md +++ b/goldens/public-api/router/index.api.md @@ -198,6 +198,11 @@ export class ChildrenOutletContexts { // @public export type ComponentInputBindingFeature = RouterFeature; +// @public +export interface ComponentInputBindingOptions { + queryParams?: boolean; +} + // @public export function convertToParamMap(params: Params): ParamMap; diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index fc8dceee41c3..6513a0d7044f 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -113,6 +113,7 @@ export { InMemoryScrollingOptions, ROUTER_CONFIGURATION, RouterConfigOptions, + ComponentInputBindingOptions, } from './router_config'; export {ROUTES} from './router_config_loader'; export {ROUTER_INITIALIZER, RouterModule} from './router_module'; diff --git a/packages/router/src/router_config.ts b/packages/router/src/router_config.ts index 4d2333c69679..50e6546d08c4 100644 --- a/packages/router/src/router_config.ts +++ b/packages/router/src/router_config.ts @@ -200,6 +200,7 @@ export interface InMemoryScrollingOptions { * @publicApi * @see withComponentInputBinding * @see RouterModule#forRoot + * @see [Disable query parameter binding](guide/routing/common-router-tasks#disable-query-parameter-binding) */ export interface ComponentInputBindingOptions { /** From 5879f310b45e9483b717a086e8dcef1430a657b4 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:54:39 -0500 Subject: [PATCH 03/15] docs: Update docs with `ComponentInputBindingOptions` --- .../guide/routing/common-router-tasks.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/adev/src/content/guide/routing/common-router-tasks.md b/adev/src/content/guide/routing/common-router-tasks.md index 743028df8d0c..f73e0f61276d 100644 --- a/adev/src/content/guide/routing/common-router-tasks.md +++ b/adev/src/content/guide/routing/common-router-tasks.md @@ -57,8 +57,24 @@ internalId = linkedSignal(() => this.id() ?? getDefaultId()); NOTE: You can bind all route data with key, value pairs to component inputs: static or resolved route data, path parameters, matrix parameters, and query parameters. + +### Disable query parameter binding + +Use `ComponentInputBindingOptions` to disable query parameter binding if you manage query parameters separately: + +```ts +provideRouter(appRoutes, withComponentInputBinding({queryParams: false})); +``` + +### Inherit parent route data + If you want to use the parent components route info you will need to set the router `paramsInheritanceStrategy` option: -`withRouterConfig({paramsInheritanceStrategy: 'always'})` . See [router configuration options](guide/routing/customizing-route-behavior#router-configuration-options) for details on other available settings. + +```ts +withRouterConfig({paramsInheritanceStrategy: 'always'}); +``` + +See [router configuration options](guide/routing/customizing-route-behavior#router-configuration-options) for details on other available settings. ## Displaying a 404 page From 37b1c5d0fe61133111962384c892c6352fbd5206 Mon Sep 17 00:00:00 2001 From: weedorflow Date: Wed, 25 Mar 2026 05:49:34 +0800 Subject: [PATCH 04/15] docs: clarify size budget configuration --- adev/src/content/tools/cli/build.md | 54 +++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/adev/src/content/tools/cli/build.md b/adev/src/content/tools/cli/build.md index a7507182ad12..8441b2d73e26 100644 --- a/adev/src/content/tools/cli/build.md +++ b/adev/src/content/tools/cli/build.md @@ -70,6 +70,7 @@ Define your size boundaries in the CLI configuration file, `angular.json`, in a You can specify size budgets for the entire app, and for particular parts. Each budget entry configures a budget of a given type. +For example, the `initial` budget measures the JavaScript and CSS used to bootstrap the application, which corresponds to the `Initial Total` value shown in the build output summary. Specify size values in the following formats: | Size value | Details | @@ -83,17 +84,48 @@ When you configure a budget, the builder warns or reports an error when a given Each budget entry is a JSON object with the following properties: -| Property | Value | -| :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| type | The type of budget. One of:
Value Details
bundle The size of a specific bundle.
initial The size of JavaScript and CSS needed for bootstrapping the application. Defaults to warning at 500kb and erroring at 1mb.
allScript The size of all scripts.
all The size of the entire application.
anyComponentStyle This size of any one component stylesheet. Defaults to warning at 2kb and erroring at 4kb.
anyScript The size of any one script.
any The size of any file.
| -| name | The name of the bundle (for `type=bundle`). | -| baseline | The baseline size for comparison. | -| maximumWarning | The maximum threshold for warning relative to the baseline. | -| maximumError | The maximum threshold for error relative to the baseline. | -| minimumWarning | The minimum threshold for warning relative to the baseline. | -| minimumError | The minimum threshold for error relative to the baseline. | -| warning | The threshold for warning relative to the baseline (min & max). | -| error | The threshold for error relative to the baseline (min & max). | +| Property | Value | +| :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| type | The type of budget. One of:
Value Details
bundle The size of a specific bundle. Use this type together with name to budget a specific bundle, including a lazy-loaded bundle.
initial The size of JavaScript and CSS needed for bootstrapping the application. This corresponds to the Initial Total value shown in the build output summary. Defaults to warning at 500kb and erroring at 1mb.
allScript The size of all scripts.
all The size of the entire application.
anyComponentStyle This size of any one component stylesheet. Defaults to warning at 2kb and erroring at 4kb.
anyScript The size of any one script.
any The size of any file.
| +| name | The bundle name for `type=bundle`. This is the bundle name reported by the builder, not the generated output filename. | +| baseline | The baseline size for comparison. If not set, the baseline defaults to `0`, and threshold values are calculated relative to that baseline. | +| maximumWarning | The maximum threshold for warning relative to the baseline. | +| maximumError | The maximum threshold for error relative to the baseline. | +| minimumWarning | The minimum threshold for warning relative to the baseline. | +| minimumError | The minimum threshold for error relative to the baseline. | +| warning | The threshold for warning relative to the baseline (min & max). | +| error | The threshold for error relative to the baseline (min & max). | + +To configure a budget for a lazy-loaded bundle, use `type: "bundle"` and set `name` to that bundle's name. + +```json +{ + "budgets": [ + { + "type": "bundle", + "name": "admin", + "maximumWarning": "250kb", + "maximumError": "300kb" + } + ] +} +``` + +The `name` field matches the bundle name, not the emitted filename, so it does not use wildcard or regular expression patterns such as `admin.*.js`. + +The following example shows a budget that uses a baseline: + +```json +{ + "type": "bundle", + "name": "main", + "baseline": "200kb", + "maximumWarning": "10%", + "maximumError": "20%" +} +``` + +In this example, the builder warns when the bundle grows beyond `220kb` and errors when it grows beyond `240kb`. ## Configuring CommonJS dependencies From ee8d2098cb3cdce1589c462cd9a66eae490477f9 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 23 Mar 2026 13:01:09 -0700 Subject: [PATCH 05/15] fix(forms): change FieldState optional properties to non-optional | undefined This improves compatibility with TypeScript's exactOptionalPropertyTypes. Fixes #67246 --- goldens/public-api/forms/signals/index.api.md | 16 ++++---- .../forms/signals/src/api/rules/metadata.ts | 12 +++--- .../api/rules/validation/validate_async.ts | 2 +- packages/forms/signals/src/api/types.ts | 8 ++-- packages/forms/signals/src/field/metadata.ts | 37 +++++++++++------- packages/forms/signals/src/field/node.ts | 3 ++ packages/forms/signals/src/schema/logic.ts | 4 ++ .../forms/signals/test/node/resource.spec.ts | 39 +++++++++++++++++-- 8 files changed, 85 insertions(+), 36 deletions(-) diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index 608d8ce09e06..e48672db9c15 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -79,10 +79,10 @@ export type CompatSchemaPath(create: (s: Signal) => TRead): MetadataKey; +export function createManagedMetadataKey(create: (state: FieldState, data: Signal) => TRead): MetadataKey; // @public -export function createManagedMetadataKey(create: (s: Signal) => TRead, reducer: MetadataReducer): MetadataKey; +export function createManagedMetadataKey(create: (state: FieldState, data: Signal) => TRead, reducer: MetadataReducer): MetadataKey; // @public export function createMetadataKey(): MetadataKey, TWrite, TWrite | undefined>; @@ -345,9 +345,9 @@ export function metadata, TPathK // @public export class MetadataKey { - protected constructor(reducer: MetadataReducer, create: ((s: Signal) => TRead) | undefined); + protected constructor(reducer: MetadataReducer, create: ((state: FieldState, data: Signal) => TRead) | undefined); // (undocumented) - readonly create: ((s: Signal) => TRead) | undefined; + readonly create: ((state: FieldState, data: Signal) => TRead) | undefined; // (undocumented) readonly reducer: MetadataReducer; } @@ -509,11 +509,11 @@ export interface ReadonlyFieldState; readonly invalid: Signal; readonly keyInParent: Signal; - readonly max?: Signal; - readonly maxLength?: Signal; + readonly max: Signal | undefined; + readonly maxLength: Signal | undefined; metadata(key: MetadataKey): M | undefined; - readonly min?: Signal; - readonly minLength?: Signal; + readonly min: Signal | undefined; + readonly minLength: Signal | undefined; readonly name: Signal; readonly pattern: Signal; readonly pending: Signal; diff --git a/packages/forms/signals/src/api/rules/metadata.ts b/packages/forms/signals/src/api/rules/metadata.ts index ef5b19d9888e..090ba4909231 100644 --- a/packages/forms/signals/src/api/rules/metadata.ts +++ b/packages/forms/signals/src/api/rules/metadata.ts @@ -9,7 +9,7 @@ import {type Signal} from '@angular/core'; import {FieldPathNode} from '../../schema/path_node'; import {assertPathIsCurrent} from '../../schema/schema'; -import type {LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types'; +import type {FieldState, LogicFn, PathKind, SchemaPath, SchemaPathRules} from '../types'; /** * Sets a value for the {@link MetadataKey} for this field. @@ -150,7 +150,7 @@ export class MetadataKey { /** Use {@link reducedMetadataKey}. */ protected constructor( readonly reducer: MetadataReducer, - readonly create: ((s: Signal) => TRead) | undefined, + readonly create: ((state: FieldState, data: Signal) => TRead) | undefined, ) {} } @@ -211,7 +211,7 @@ export function createMetadataKey( * @experimental 21.0.0 */ export function createManagedMetadataKey( - create: (s: Signal) => TRead, + create: (state: FieldState, data: Signal) => TRead, ): MetadataKey; /** * Creates a metadata key that exposes a managed value based on the accumulated result of the values @@ -229,16 +229,16 @@ export function createManagedMetadataKey( * @experimental 21.0.0 */ export function createManagedMetadataKey( - create: (s: Signal) => TRead, + create: (state: FieldState, data: Signal) => TRead, reducer: MetadataReducer, ): MetadataKey; export function createManagedMetadataKey( - create: (s: Signal) => TRead, + create: (state: FieldState, data: Signal) => TRead, reducer?: MetadataReducer, ): MetadataKey { return new (MetadataKey as new ( reducer: MetadataReducer, - create: (s: Signal) => TRead, + create: (state: FieldState, data: Signal) => TRead, ) => MetadataKey)(reducer ?? MetadataReducer.override(), create); } diff --git a/packages/forms/signals/src/api/rules/validation/validate_async.ts b/packages/forms/signals/src/api/rules/validation/validate_async.ts index 0ef8f5d92015..b6c16890f936 100644 --- a/packages/forms/signals/src/api/rules/validation/validate_async.ts +++ b/packages/forms/signals/src/api/rules/validation/validate_async.ts @@ -118,7 +118,7 @@ export function validateAsync, TParams | undefined>( - opts.factory, + (_state, params) => opts.factory(params), ); RESOURCE[IS_ASYNC_VALIDATION_RESOURCE] = true; diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index b624f350cb04..0c5ed44d42a6 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -346,28 +346,28 @@ export interface ReadonlyFieldState` with a numeric or date `type` attribute and custom controls. */ - readonly max?: Signal; + readonly max: Signal | undefined; /** * A signal indicating the field's maximum string length, if applicable. * * Applies to ``, `