refactor(core): implement @boundary runtime primitives and AST nodes (#70463)

Add the runtime primitives `ɵɵboundaryCreate` and `ɵɵboundaryUpdate` to
the core instructions, which handle synchronous view destruction and
provide the `ON_ERROR` interceptor hooks.

Also include the initial compiler AST representations for the new syntax
including the Lexer tokenization and HTML Parser integration. This lays
the foundational structure for `@boundary` prior to code generation.

Co-authored-by: Matthieu Riegler <kyro38@gmail.com>

PR Close #70463
This commit is contained in:
Alex Rickabaugh
2026-03-17 17:04:06 -07:00
committed by Kristiyan Kostadinov
parent f6afb807c1
commit 54ed62d240
66 changed files with 2060 additions and 179 deletions
+1 -1
View File
@@ -713,7 +713,7 @@ export type EnvironmentProviders = {
// @public
export interface ErrorDetails {
readonly boundary?: {
readonly type: Type<any>;
readonly type: Type<unknown>;
readonly reset: () => void;
};
readonly caughtBy?: Function;
@@ -0,0 +1,273 @@
/****************************************************************************************************
* PARTIAL FILE: basic.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error {
Fallback Content
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error {
Fallback Content
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: basic.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: error_declaration.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error (let err) {
Error: {{err.message}}
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error (let err) {
Error: {{err.message}}
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: error_declaration.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: retry_alias.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error (let err, retry = $reset) {
<button (click)="retry()">Retry</button>
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error (let err, retry = $reset) {
<button (click)="retry()">Retry</button>
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: retry_alias.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: retry_implicit.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error {
<button (click)="$reset()">Retry</button>
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error {
<button (click)="$reset()">Retry</button>
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: retry_implicit.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: when_condition.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: when_condition.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: multiple_error_blocks.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
} @error (let err) {
Generic Error
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
} @error (let err) {
Generic Error
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: multiple_error_blocks.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
/****************************************************************************************************
* PARTIAL FILE: nested.js
****************************************************************************************************/
import { Component } from '@angular/core';
import * as i0 from "@angular/core";
export class TestComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: TestComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
@boundary {
Outer Main
@boundary {
Inner Main
} @error {
Inner Fallback
}
} @error {
Outer Fallback
}
`, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestComponent, decorators: [{
type: Component,
args: [{
template: `
@boundary {
Outer Main
@boundary {
Inner Main
} @error {
Inner Fallback
}
} @error {
Outer Fallback
}
`
}]
}] });
/****************************************************************************************************
* PARTIAL FILE: nested.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,75 @@
{
"$schema": "../../test_case_schema.json",
"cases": [
{
"description": "should generate boundary and error blocks",
"inputFiles": ["basic.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions",
"files": ["basic.js"]
}
]
},
{
"description": "should support let declaration of the error",
"inputFiles": ["error_declaration.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions for error declaration",
"files": ["error_declaration.js"]
}
]
},
{
"description": "should support retry function alias",
"inputFiles": ["retry_alias.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions for retry alias",
"files": ["retry_alias.js"]
}
]
},
{
"description": "should support retry implicitly",
"inputFiles": ["retry_implicit.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions",
"files": ["retry_implicit.js"]
}
]
},
{
"description": "should support when conditions",
"inputFiles": ["when_condition.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions for when condition",
"files": ["when_condition.js"]
}
]
},
{
"description": "should support multiple error blocks",
"inputFiles": ["multiple_error_blocks.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions for multiple error blocks",
"files": ["multiple_error_blocks.js"]
}
]
},
{
"description": "should support nested boundaries",
"inputFiles": ["nested.ts"],
"expectations": [
{
"failureMessage": "Incorrect boundary instructions for nested boundaries",
"files": ["nested.js"]
}
]
}
]
}
@@ -0,0 +1,20 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Fallback Content ");
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 1, 0);
}
if (rf & 2) {
$r3$.ɵɵboundaryUpdate(0, $r3$.ɵɵgetBoundary(0).error === null ? 1 : 2, 1);
}
}
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error {
Fallback Content
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,23 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0);
}
if (rf & 2) {
$r3$.ɵɵtextInterpolate1(" Error: ", ctx.$error.message, " ");
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 1, 1);
}
if (rf & 2) {
$r3$.ɵɵboundaryUpdate(0, $r3$.ɵɵgetBoundary(0).error === null ? 1 : 2, 1);
}
}
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error (let err) {
Error: {{err.message}}
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,26 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Not Found ");
}
}
function TestComponent_Error_3_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Generic Error ");
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 1, 0)(3, TestComponent_Error_3_Template, 1, 0);
}
if (rf & 2) {
const $bnd_r1$ = $r3$.ɵɵgetBoundary(0);
$r3$.ɵɵboundaryUpdate(0, $bnd_r1$.error === null ? 1 : $bnd_r1$.error.message === "404" ? 2 : 3, 1);
}
}
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
} @error (let err) {
Generic Error
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,36 @@
function TestComponent_Primary_1_Primary_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Inner Main ");
}
}
function TestComponent_Primary_1_Error_3_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Inner Fallback ");
}
}
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Outer Main ");
$r3$.ɵɵboundaryCreate(1);
$r3$.ɵɵconditionalBranchCreate(2, TestComponent_Primary_1_Primary_2_Template, 1, 0)(3, TestComponent_Primary_1_Error_3_Template, 1, 0);
}
if (rf & 2) {
$r3$.ɵɵadvance();
$r3$.ɵɵboundaryUpdate(1, $r3$.ɵɵgetBoundary(1).error === null ? 2 : 3, 2);
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Outer Fallback ");
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 4, 1)(2, TestComponent_Error_2_Template, 1, 0);
}
if (rf & 2) {
$r3$.ɵɵboundaryUpdate(0, $r3$.ɵɵgetBoundary(0).error === null ? 1 : 2, 1);
}
}
@@ -0,0 +1,17 @@
import {Component} from '@angular/core';
@Component({
template: `
@boundary {
Outer Main
@boundary {
Inner Main
} @error {
Inner Fallback
}
} @error {
Outer Fallback
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,27 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
const $r1$ = $r3$.ɵɵgetCurrentView();
$r3$.ɵɵdomElementStart(0, "button", 0);
$r3$.ɵɵdomListener("click", function TestComponent_Error_2_Template_button_click_0_listener() {
const $ctx_r1$ = $r3$.ɵɵrestoreView($r1$);
return $r3$.ɵɵresetView($ctx_r1$.$reset());
});
$r3$.ɵɵtext(1, "Retry");
$r3$.ɵɵdomElementEnd();
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 2, 0);
}
if (rf & 2) {
$r3$.ɵɵboundaryUpdate(0, $r3$.ɵɵgetBoundary(0).error === null ? 1 : 2, 1);
}
}
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error (let err, retry = $reset) {
<button (click)="retry()">Retry</button>
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,27 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
const $r1$ = $r3$.ɵɵgetCurrentView();
$r3$.ɵɵdomElementStart(0, "button", 0);
$r3$.ɵɵdomListener("click", function TestComponent_Error_2_Template_button_click_0_listener() {
const $ctx_r1$ = $r3$.ɵɵrestoreView($r1$);
return $r3$.ɵɵresetView($ctx_r1$.$reset());
});
$r3$.ɵɵtext(1, "Retry");
$r3$.ɵɵdomElementEnd();
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 2, 0);
}
if (rf & 2) {
$r3$.ɵɵboundaryUpdate(0, $r3$.ɵɵgetBoundary(0).error === null ? 1 : 2, 1);
}
}
@@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error {
<button (click)="$reset()">Retry</button>
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -0,0 +1,21 @@
function TestComponent_Primary_1_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Main Content ");
}
}
function TestComponent_Error_2_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtext(0, " Not Found ");
}
}
…
function TestComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵboundaryCreate(0);
$r3$.ɵɵconditionalBranchCreate(1, TestComponent_Primary_1_Template, 1, 0)(2, TestComponent_Error_2_Template, 1, 0);
}
if (rf & 2) {
const $bnd_r1$ = $r3$.ɵɵgetBoundary(0);
$r3$.ɵɵboundaryUpdate(0, $bnd_r1$.error === null ? 1 : $bnd_r1$.error.message === "404" ? 2 : -1, 1);
}
}
@@ -0,0 +1,12 @@
import {Component} from '@angular/core';
@Component({
template: `
@boundary {
Main Content
} @error (let err; when err.message === '404') {
Not Found
}
`
})
export class TestComponent {}
@@ -0,0 +1,5 @@
import * as i0 from "@angular/core";
export declare class TestComponent {
static ɵfac: i0.ɵɵFactoryDeclaration<TestComponent, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<TestComponent, "ng-component", never, {}, {}, never, never, true, never>;
}
@@ -20,14 +20,14 @@ const allUpdateTargets = execSync(queryCommand, {encoding: 'utf-8', stdio: 'pipe
.filter((target) => target.length > 0);
if (allUpdateTargets.length === 0) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
if (process.stdout.clearLine) process.stdout.clearLine();
if (process.stdout.cursorTo) process.stdout.cursorTo(0);
console.error(`Could not find any symbol test targets using: ${queryCommand}`);
process.exit(1);
}
process.stdout.clearLine();
process.stdout.cursorTo(0);
if (process.stdout.clearLine) process.stdout.clearLine();
if (process.stdout.cursorTo) process.stdout.cursorTo(0);
for (const [index, target] of allUpdateTargets.entries()) {
const progress = `${index + 1} / ${allUpdateTargets.length}`;
@@ -36,8 +36,8 @@ for (const [index, target] of allUpdateTargets.entries()) {
stdio: 'pipe',
encoding: 'utf-8',
});
process.stdout.clearLine();
process.stdout.cursorTo(0);
if (process.stdout.clearLine) process.stdout.clearLine();
if (process.stdout.cursorTo) process.stdout.cursorTo(0);
if (commandResult.status) {
console.error(`[${progress}] Failed run: ${target}`);
console.group();
@@ -0,0 +1,271 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {NgtscTestEnvironment} from './env';
const testFiles = loadStandardTestFiles({fakeCommon: true});
runInEachFileSystem(() => {
describe('ngtsc @boundary type checking', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true});
});
it('should type check error alias as Error', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error (let err) {
<div>{{ err.message }}</div>
}
\`,
standalone: true,
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should error when accessing non-existent properties on error alias', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error (let err) {
<div>{{ err.nonExistentProperty }}</div>
}
\`,
standalone: true,
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain(
"Property 'nonExistentProperty' does not exist on type 'Error'",
);
});
it('should narrowing type using condition when available', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
class CustomError extends Error {
customField = 'test';
}
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error (let err; when err instanceof CustomError) {
<div>{{ err.customField }}</div>
} @error (let err) {
<div>Fallback</div>
}
\`,
standalone: true,
})
export class TestCmp {
CustomError = CustomError; // Expose to template if needed, though with control flow it uses standard TS scope in some regards
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should report an error if there are multiple unconditional @error blocks', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error {
<div>Fallback 1</div>
} @error {
<div>Fallback 2</div>
}
\`,
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
// It will report two errors: one because the first block isn't the last, and one for the duplicate
expect(diags.length).toBe(2);
expect(diags[0].messageText).toContain(
'Unconditional @error block must be the last @error block in the boundary chain',
);
expect(diags[1].messageText).toContain(
'@boundary block can only have one unconditional @error block',
);
});
it('should report an error if an unconditional @error block is not the last block', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
class CustomError extends Error {}
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error {
<div>Fallback</div>
} @error (let err; when err instanceof CustomError) {
<div>Custom Error</div>
}
\`,
})
export class TestCmp {
CustomError = CustomError;
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain(
'Unconditional @error block must be the last @error block in the boundary chain',
);
});
it('should error when accessing error alias outside of error block', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal {{ err.message }}</div>
} @error (let err) {
<div>{{ err.message }}</div>
}
\`,
})
export class TestCmp {}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain("Property 'err' does not exist on type 'TestCmp'");
});
it('should correctly handle nested boundary blocks and shadow error alias', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
class CustomError extends Error {
customField = 'test';
}
@Component({
selector: 'test-cmp',
template: \`
@boundary {
@boundary {
<div>Inner</div>
} @error (let err; when err instanceof CustomError) {
<div>Inner Custom: {{ err.customField }}</div>
} @error (let err) {
<div>Inner Fallback: {{ err.message }}</div>
}
} @error (let err) {
<div>Outer Fallback: {{ err.message }}</div>
}
\`,
})
export class TestCmp {
CustomError = CustomError;
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
it('should allow multiple error blocks with different conditions', () => {
env.write(
'test.ts',
`
import {Component} from '@angular/core';
class ErrorA extends Error { type = 'a' }
class ErrorB extends Error { type = 'b' }
@Component({
selector: 'test-cmp',
template: \`
@boundary {
<div>Normal</div>
} @error (let err; when err instanceof ErrorA) {
<div>A: {{ err.type }}</div>
} @error (let err; when err instanceof ErrorB) {
<div>B: {{ err.type }}</div>
} @error (let err) {
<div>Generic: {{ err.message }}</div>
}
\`,
})
export class TestCmp {
ErrorA = ErrorA;
ErrorB = ErrorB;
}
`,
);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
});
});
+11
View File
@@ -130,6 +130,17 @@ export class CombinedRecursiveAstVisitor extends RecursiveAstVisitor implements
this.visitAllTemplateNodes(block.children);
}
visitBoundaryBlock(block: t.BoundaryBlock): void {
this.visitAllTemplateNodes(block.children);
this.visitAllTemplateNodes(block.errorBlocks);
}
visitBoundaryErrorBlock(block: t.BoundaryErrorBlock): void {
this.visitAllTemplateNodes(block.contextVariables);
block.expression && this.visit(block.expression);
this.visitAllTemplateNodes(block.children);
}
visitLetDeclaration(decl: t.LetDeclaration): void {
this.visit(decl.value);
}
+2
View File
@@ -152,6 +152,8 @@ export {
BoundDeferredTrigger as TmplAstBoundDeferredTrigger,
BoundEvent as TmplAstBoundEvent,
BoundText as TmplAstBoundText,
BoundaryBlock as TmplAstBoundaryBlock,
BoundaryErrorBlock as TmplAstBoundaryErrorBlock,
Content as TmplAstContent,
DeferredBlock as TmplAstDeferredBlock,
DeferredBlockError as TmplAstDeferredBlockError,
+1
View File
@@ -152,6 +152,7 @@ const SUPPORTED_BLOCKS = [
'@defer',
'@placeholder',
'@loading',
'@boundary',
'@error',
'@content',
] as const;
+48
View File
@@ -358,6 +358,44 @@ export class ContentBlock extends BlockNode implements Node {
}
}
export class BoundaryBlock extends BlockNode implements Node {
constructor(
public children: Node[],
public errorBlocks: BoundaryErrorBlock[],
nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan,
public mainBlockSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan | null,
public i18n?: I18nMeta,
) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitBoundaryBlock(this);
}
}
export class BoundaryErrorBlock extends BlockNode implements Node {
constructor(
public children: Node[],
public contextVariables: Variable[],
public expression: AST | null,
nameSpan: ParseSourceSpan,
sourceSpan: ParseSourceSpan,
startSourceSpan: ParseSourceSpan,
endSourceSpan: ParseSourceSpan | null,
public i18n?: I18nMeta,
) {
super(nameSpan, sourceSpan, startSourceSpan, endSourceSpan);
}
visit<Result>(visitor: Visitor<Result>): Result {
return visitor.visitBoundaryErrorBlock(this);
}
}
export interface DeferredBlockTriggers {
when?: BoundDeferredTrigger;
idle?: IdleDeferredTrigger;
@@ -778,6 +816,8 @@ export interface Visitor<Result = any> {
visitForLoopBlockEmpty(block: ForLoopBlockEmpty): Result;
visitIfBlock(block: IfBlock): Result;
visitIfBlockBranch(block: IfBlockBranch): Result;
visitBoundaryBlock(block: BoundaryBlock): Result;
visitBoundaryErrorBlock(block: BoundaryErrorBlock): Result;
visitUnknownBlock(block: UnknownBlock): Result;
visitLetDeclaration(decl: LetDeclaration): Result;
visitComponent(component: Component): Result;
@@ -839,6 +879,14 @@ export class RecursiveVisitor implements Visitor<void> {
visitAll(this, block.children);
block.expressionAlias?.visit(this);
}
visitBoundaryBlock(block: BoundaryBlock): void {
visitAll(this, block.children);
visitAll(this, block.errorBlocks);
}
visitBoundaryErrorBlock(block: BoundaryErrorBlock): void {
const blockItems = [...block.contextVariables, ...block.children];
visitAll(this, blockItems);
}
visitContent(content: Content): void {
visitAll(this, content.children);
}
@@ -0,0 +1,168 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {AST} from '../expression_parser/ast';
import * as html from '../ml_parser/ast';
import {ParseError, ParseSourceSpan} from '../parse_util';
import {BindingParser} from '../template_parser/binding_parser';
import * as t from './r3_ast';
import {IDENTIFIER_PATTERN, LET_PATTERN, parseLetParameters} from './util';
/** Pattern used to identify a boundary `when` expression. */
const WHEN_PATTERN = /^(when\s+)(.*)/;
export function isConnectedBoundaryErrorBlock(name: string): boolean {
return name === 'error';
}
export function createBoundaryBlock(
ast: html.Block,
connectedBlocks: html.Block[],
visitor: html.Visitor,
bindingParser: BindingParser,
): {node: t.BoundaryBlock | null; errors: ParseError[]} {
const errors: ParseError[] = [];
const errorBlocks: t.BoundaryErrorBlock[] = [];
if (ast.parameters.length > 0) {
errors.push(new ParseError(ast.sourceSpan, '@boundary block cannot have parameters'));
}
for (let blockIndex = 0; blockIndex < connectedBlocks.length; blockIndex++) {
const block = connectedBlocks[blockIndex];
if (block.name !== 'error') {
errors.push(
new ParseError(block.sourceSpan, `Unrecognized @boundary connected block @${block.name}`),
);
continue;
}
const emptySpan = new ParseSourceSpan(block.startSourceSpan.end, block.startSourceSpan.end);
const contextVariables: t.Variable[] = [
new t.Variable('$error', '$error', emptySpan, emptySpan, emptySpan),
new t.Variable('$reset', '$reset', emptySpan, emptySpan, emptySpan),
];
let expression: AST | null = null;
for (const param of block.parameters) {
const letMatch = param.expression.match(LET_PATTERN);
const isAliasMatch = param.expression.match(/^\s*[$A-Z_][0-9A-Z_$]*\s*=/i) !== null;
if (letMatch !== null || isAliasMatch) {
const expressionToParse = letMatch !== null ? letMatch[1] : param.expression;
const variablesSpan =
letMatch !== null
? new ParseSourceSpan(
param.sourceSpan.start.moveBy(letMatch[0].length - letMatch[1].length),
param.sourceSpan.end,
)
: param.sourceSpan;
parseLetParameters(
param.sourceSpan,
expressionToParse,
variablesSpan,
contextVariables,
errors,
(name, variableName, sourceSpan) => {
if (variableName !== '$error' && variableName !== '$reset') {
errors.push(
new ParseError(
sourceSpan,
`Unknown context variable "${variableName}". Only "$error" and "$reset" are allowed`,
),
);
} else if (contextVariables.some((v) => v.name === name)) {
errors.push(
new ParseError(sourceSpan, `Duplicate "let" parameter variable "${name}"`),
);
}
},
'@error block',
'$error',
);
continue;
}
const whenMatch = param.expression.match(WHEN_PATTERN);
if (whenMatch) {
if (expression !== null) {
errors.push(
new ParseError(param.sourceSpan, '@error block can only have one "when" expression'),
);
} else {
const start = param.expression.indexOf(whenMatch[2]);
const end = start + whenMatch[2].length;
const expressionAST = bindingParser.parseBinding(
param.expression.slice(start, end),
false,
param.sourceSpan,
param.sourceSpan.start.offset + start,
);
expression = expressionAST.ast;
}
continue;
}
errors.push(
new ParseError(
param.sourceSpan,
`Unrecognized @error block parameter "${param.expression}"`,
),
);
}
errorBlocks.push(
new t.BoundaryErrorBlock(
html.visitAll(visitor, block.children, block.children),
contextVariables,
expression,
block.nameSpan,
block.sourceSpan,
block.startSourceSpan,
block.endSourceSpan,
block.i18n,
),
);
}
let hasUnconditionalErrorBlock = false;
for (let i = 0; i < errorBlocks.length; i++) {
const errorBlock = errorBlocks[i];
if (errorBlock.expression === null) {
if (hasUnconditionalErrorBlock) {
errors.push(
new ParseError(
errorBlock.sourceSpan,
'@boundary block can only have one unconditional @error block',
),
);
} else if (i !== errorBlocks.length - 1) {
errors.push(
new ParseError(
errorBlock.sourceSpan,
'Unconditional @error block must be the last @error block in the boundary chain',
),
);
}
hasUnconditionalErrorBlock = true;
}
}
const node = new t.BoundaryBlock(
html.visitAll(visitor, ast.children, ast.children),
errorBlocks,
ast.nameSpan,
ast.sourceSpan,
ast.startSourceSpan,
ast.endSourceSpan,
ast.i18n,
);
return {node, errors};
}
@@ -12,7 +12,7 @@ import {ParseError, ParseSourceSpan} from '../parse_util';
import {BindingParser} from '../template_parser/binding_parser';
import * as t from './r3_ast';
import {IDENTIFIER_PATTERN, LET_PATTERN} from './util';
import {IDENTIFIER_PATTERN, LET_PATTERN, parseLetParameters} from './util';
/** Pattern for the expression in a for loop block. */
const FOR_LOOP_EXPRESSION_PATTERN = /^\s*([0-9A-Za-z_$]*)\s+of\s+([\S\s]*)/;
@@ -23,12 +23,6 @@ const FOR_LOOP_TRACK_PATTERN = /^track\s+([\S\s]*)/;
/** Pattern for the `as` expression in a conditional block. */
const CONDITIONAL_ALIAS_PATTERN = /^(as\s+)(.*)/;
/**
* Pattern to group a string into leading whitespace, non whitespace, and trailing whitespace.
* Useful for getting the variable name span when a span can contain leading and trailing space.
*/
const CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN = /(\s*)(\S+)(\s*)/;
/** Names of variables that are allowed to be used in the `let` expression of a `for` loop. */
const ALLOWED_FOR_LOOP_LET_VARIABLES = new Set([
'$index',
@@ -430,13 +424,36 @@ function parseForLoopParameters(
param.sourceSpan.start.moveBy(letMatch[0].length - letMatch[1].length),
param.sourceSpan.end,
);
parseLetParameter(
parseLetParameters(
param.sourceSpan,
letMatch[1],
variablesSpan,
itemName,
result.context,
errors,
(name, variableName, sourceSpan) => {
if (!ALLOWED_FOR_LOOP_LET_VARIABLES.has(variableName)) {
errors.push(
new ParseError(
sourceSpan,
`Unknown "let" parameter variable "${variableName}". The allowed variables are: ${Array.from(
ALLOWED_FOR_LOOP_LET_VARIABLES,
).join(', ')}`,
),
);
} else if (name === itemName) {
errors.push(
new ParseError(
sourceSpan,
`Invalid @for loop "let" parameter. Variable cannot be called "${itemName}"`,
),
);
} else if (result.context.some((v) => v.name === name)) {
errors.push(
new ParseError(sourceSpan, `Duplicate "let" parameter variable "${variableName}"`),
);
}
},
'@for loop',
);
continue;
}
@@ -484,83 +501,6 @@ function validateTrackByExpression(
}
}
/** Parses the `let` parameter of a `for` loop block. */
function parseLetParameter(
sourceSpan: ParseSourceSpan,
expression: string,
span: ParseSourceSpan,
loopItemName: string,
context: t.Variable[],
errors: ParseError[],
): void {
const parts = expression.split(',');
let startSpan = span.start;
for (const part of parts) {
const expressionParts = part.split('=');
const name = expressionParts.length === 2 ? expressionParts[0].trim() : '';
const variableName = expressionParts.length === 2 ? expressionParts[1].trim() : '';
if (name.length === 0 || variableName.length === 0) {
errors.push(
new ParseError(
sourceSpan,
`Invalid @for loop "let" parameter. Parameter should match the pattern "<name> = <variable name>"`,
),
);
} else if (!ALLOWED_FOR_LOOP_LET_VARIABLES.has(variableName)) {
errors.push(
new ParseError(
sourceSpan,
`Unknown "let" parameter variable "${variableName}". The allowed variables are: ${Array.from(
ALLOWED_FOR_LOOP_LET_VARIABLES,
).join(', ')}`,
),
);
} else if (name === loopItemName) {
errors.push(
new ParseError(
sourceSpan,
`Invalid @for loop "let" parameter. Variable cannot be called "${loopItemName}"`,
),
);
} else if (context.some((v) => v.name === name)) {
errors.push(
new ParseError(sourceSpan, `Duplicate "let" parameter variable "${variableName}"`),
);
} else {
const [, keyLeadingWhitespace, keyName] =
expressionParts[0].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
const keySpan =
keyLeadingWhitespace !== undefined && expressionParts.length === 2
? new ParseSourceSpan(
/* strip leading spaces */
startSpan.moveBy(keyLeadingWhitespace.length),
/* advance to end of the variable name */
startSpan.moveBy(keyLeadingWhitespace.length + keyName.length),
)
: span;
let valueSpan: ParseSourceSpan | undefined = undefined;
if (expressionParts.length === 2) {
const [, valueLeadingWhitespace, implicit] =
expressionParts[1].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
valueSpan =
valueLeadingWhitespace !== undefined
? new ParseSourceSpan(
startSpan.moveBy(expressionParts[0].length + 1 + valueLeadingWhitespace.length),
startSpan.moveBy(
expressionParts[0].length + 1 + valueLeadingWhitespace.length + implicit.length,
),
)
: undefined;
}
const sourceSpan = new ParseSourceSpan(keySpan.start, valueSpan?.end ?? keySpan.end);
context.push(new t.Variable(name, variableName, sourceSpan, keySpan, valueSpan));
}
startSpan = startSpan.moveBy(part.length + 1 /* add 1 to move past the comma */);
}
}
/**
* Checks that the shape of the blocks connected to an
* `@if` block is correct. Returns an array of errors.
@@ -202,6 +202,9 @@ export class Identifiers {
moduleName: CORE,
};
static conditional: o.ExternalReference = {name: 'ɵɵconditional', moduleName: CORE};
static boundaryCreate: o.ExternalReference = {name: 'ɵɵboundaryCreate', moduleName: CORE};
static boundaryUpdate: o.ExternalReference = {name: 'ɵɵboundaryUpdate', moduleName: CORE};
static getBoundary: o.ExternalReference = {name: 'ɵɵgetBoundary', moduleName: CORE};
static repeater: o.ExternalReference = {name: 'ɵɵrepeater', moduleName: CORE};
static repeaterCreate: o.ExternalReference = {name: 'ɵɵrepeaterCreate', moduleName: CORE};
static repeaterTrackByIndex: o.ExternalReference = {
@@ -25,6 +25,7 @@ import {BindingParser} from '../template_parser/binding_parser';
import {PreparsedElementType, preparseElement} from '../template_parser/template_preparser';
import * as t from './r3_ast';
import {createBoundaryBlock, isConnectedBoundaryErrorBlock} from './r3_boundaries';
import {createContentBlock} from './r3_content_blocks';
import {
createForLoop,
@@ -525,10 +526,22 @@ class HtmlAstToIvyAst implements html.Visitor {
);
break;
case 'boundary':
result = createBoundaryBlock(
block,
this.findConnectedBlocks(index, context, isConnectedBoundaryErrorBlock),
this,
this.bindingParser,
);
break;
default:
let errorMessage: string;
if (isConnectedDeferLoopBlock(block.name)) {
if (isConnectedBoundaryErrorBlock(block.name)) {
errorMessage = `@${block.name} block can only be used after an @defer or @boundary block.`;
this.processedNodes.add(block);
} else if (isConnectedDeferLoopBlock(block.name)) {
errorMessage = `@${block.name} block can only be used after an @defer block.`;
this.processedNodes.add(block);
} else if (isConnectedForLoopBlock(block.name)) {
+87
View File
@@ -9,8 +9,17 @@
import {escapeIdentifier} from '../output/abstract_emitter';
import * as o from '../output/output_ast';
import {ParseError, ParseSourceSpan} from '../parse_util';
import * as t from './r3_ast';
import {Identifiers} from './r3_identifiers';
/**
* Pattern to group a string into leading whitespace, non whitespace, and trailing whitespace.
* Useful for getting the variable name span when a span can contain leading and trailing space.
*/
const CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN = /(\s*)(\S+)(\s*)/;
/** Regex that includes unsafe characters in an object literal property name. */
const UNSAFE_OBJECT_KEY_NAME_REGEXP = /[-.]/;
@@ -20,6 +29,84 @@ export const IDENTIFIER_PATTERN = /^[$A-Z_][0-9A-Z_$]*$/i;
/** Pattern used to identify a `let` parameter. */
export const LET_PATTERN = /^let\s+([\S\s]*)/;
/**
* Parses the `let` parameter of a `@for` or `@error` block.
*
* @param sourceSpan The source span of the entire `let` parameter.
* @param expression The expression string of the `let` parameter, e.g., `"foo = $implicit, bar = $index"`.
* @param span The source span of the expression string.
* @param context The variable context to append parsed variables to.
* @param errors The array of parsing errors to append to.
* @param messagePrefix The prefix to use in error messages (e.g. '@for loop').
* @param defaultImplicitVariableName An optional default variable name to use if no value is provided.
*/
export function parseLetParameters(
sourceSpan: ParseSourceSpan,
expression: string,
span: ParseSourceSpan,
context: t.Variable[],
errors: ParseError[],
validateLet: (name: string, variableName: string, sourceSpan: ParseSourceSpan) => void,
messagePrefix: string,
defaultImplicitVariableName?: string,
): void {
const parts = expression.split(',');
let startSpan = span.start;
for (const part of parts) {
const expressionParts = part.split('=');
const name = expressionParts[0].trim();
let variableName =
expressionParts.length === 2
? expressionParts[1].trim()
: (defaultImplicitVariableName ?? '');
if (name.length === 0 || variableName.length === 0) {
errors.push(
new ParseError(
sourceSpan,
`Invalid ${messagePrefix} "let" parameter. Parameter should match the pattern "<name> = <variable name>"`,
),
);
} else if (!IDENTIFIER_PATTERN.test(name)) {
errors.push(
new ParseError(sourceSpan, `"let" parameter must be a valid JavaScript identifier`),
);
} else {
validateLet(name, variableName, sourceSpan);
const [, keyLeadingWhitespace, keyName] =
expressionParts[0].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
const keySpan =
keyLeadingWhitespace !== undefined
? new ParseSourceSpan(
/* strip leading spaces */
startSpan.moveBy(keyLeadingWhitespace.length),
/* advance to end of the variable name */
startSpan.moveBy(keyLeadingWhitespace.length + keyName.length),
)
: span;
let valueSpan: ParseSourceSpan | undefined = undefined;
if (expressionParts.length === 2) {
const [, valueLeadingWhitespace, implicit] =
expressionParts[1].match(CHARACTERS_IN_SURROUNDING_WHITESPACE_PATTERN) ?? [];
valueSpan =
valueLeadingWhitespace !== undefined
? new ParseSourceSpan(
startSpan.moveBy(expressionParts[0].length + 1 + valueLeadingWhitespace.length),
startSpan.moveBy(
expressionParts[0].length + 1 + valueLeadingWhitespace.length + implicit.length,
),
)
: undefined;
}
const variableSpan = new ParseSourceSpan(keySpan.start, valueSpan?.end ?? keySpan.end);
context.push(new t.Variable(name, variableName, variableSpan, keySpan, valueSpan));
}
startSpan = startSpan.moveBy(part.length + 1 /* add 1 to move past the comma */);
}
}
export function typeWithParameters(type: o.Expression, numParams: number): o.ExpressionType {
if (numParams === 0) {
return o.expressionType(type);
@@ -15,6 +15,8 @@ import {
SafePropertyRead,
} from '../../expression_parser/ast';
import {
BoundaryBlock,
BoundaryErrorBlock,
BoundAttribute,
BoundEvent,
BoundText,
@@ -54,6 +56,7 @@ import {
} from '../r3_ast';
import {CombinedRecursiveAstVisitor} from '../../combined_visitor';
import {ClassPropertyMapping, ClassPropertyName, InputOrOutput} from '../../property_mapping';
import {
BoundTarget,
ConflictingHostDirectiveBinding,
@@ -69,7 +72,6 @@ import {
} from './t2_api';
import {parseTemplate} from './template';
import {createCssSelectorFromNode} from './util';
import {ClassPropertyMapping, ClassPropertyName, InputOrOutput} from '../../property_mapping';
/**
* Computes a difference between full list (first argument) and
@@ -350,6 +352,9 @@ class Scope implements Visitor {
this.visitVariable(nodeOrNodes.item);
nodeOrNodes.contextVariables.forEach((v) => this.visitVariable(v));
nodeOrNodes.children.forEach((node) => node.visit(this));
} else if (nodeOrNodes instanceof BoundaryErrorBlock) {
nodeOrNodes.contextVariables.forEach((v) => this.visitVariable(v));
nodeOrNodes.children.forEach((node) => node.visit(this));
} else if (
nodeOrNodes instanceof SwitchBlockCaseGroup ||
nodeOrNodes instanceof ForLoopBlockEmpty ||
@@ -358,6 +363,7 @@ class Scope implements Visitor {
nodeOrNodes instanceof DeferredBlockPlaceholder ||
nodeOrNodes instanceof DeferredBlockLoading ||
nodeOrNodes instanceof ContentBlock ||
nodeOrNodes instanceof BoundaryBlock ||
nodeOrNodes instanceof Content
) {
nodeOrNodes.children.forEach((node) => node.visit(this));
@@ -412,6 +418,15 @@ class Scope implements Visitor {
this.ingestScopedNode(block);
}
visitBoundaryBlock(block: BoundaryBlock) {
this.ingestScopedNode(block);
block.errorBlocks.forEach((node) => node.visit(this));
}
visitBoundaryErrorBlock(block: BoundaryErrorBlock) {
this.ingestScopedNode(block);
}
visitSwitchBlock(block: SwitchBlock) {
block.groups.forEach((node) => node.visit(this));
}
@@ -639,6 +654,15 @@ class DirectiveBinder<DirectiveT extends DirectiveMeta> implements Visitor {
block.children.forEach((child) => child.visit(this));
}
visitBoundaryBlock(block: BoundaryBlock): void {
block.children.forEach((child) => child.visit(this));
block.errorBlocks.forEach((node) => node.visit(this));
}
visitBoundaryErrorBlock(block: BoundaryErrorBlock): void {
block.children.forEach((child) => child.visit(this));
}
visitSwitchBlock(block: SwitchBlock) {
block.groups.forEach((node) => node.visit(this));
}
@@ -1067,6 +1091,11 @@ class TemplateBinder extends CombinedRecursiveAstVisitor {
this.deferBlocks.push([nodeOrNodes, this.scope]);
nodeOrNodes.children.forEach((node) => node.visit(this));
this.nestingLevel.set(nodeOrNodes, this.level);
} else if (nodeOrNodes instanceof BoundaryErrorBlock) {
nodeOrNodes.contextVariables.forEach((v) => this.visitNode(v));
nodeOrNodes.expression?.visit(this);
nodeOrNodes.children.forEach((node) => node.visit(this));
this.nestingLevel.set(nodeOrNodes, this.level);
} else if (
nodeOrNodes instanceof SwitchBlockCaseGroup ||
nodeOrNodes instanceof ForLoopBlockEmpty ||
@@ -1074,6 +1103,7 @@ class TemplateBinder extends CombinedRecursiveAstVisitor {
nodeOrNodes instanceof DeferredBlockPlaceholder ||
nodeOrNodes instanceof DeferredBlockLoading ||
nodeOrNodes instanceof ContentBlock ||
nodeOrNodes instanceof BoundaryBlock ||
nodeOrNodes instanceof Content
) {
nodeOrNodes.children.forEach((node) => node.visit(this));
@@ -1136,6 +1166,15 @@ class TemplateBinder extends CombinedRecursiveAstVisitor {
this.ingestScopedNode(block);
}
override visitBoundaryBlock(block: BoundaryBlock) {
this.ingestScopedNode(block);
block.errorBlocks.forEach((node) => node.visit(this));
}
override visitBoundaryErrorBlock(block: BoundaryErrorBlock) {
this.ingestScopedNode(block);
}
override visitSwitchBlockCase(block: SwitchBlockCase) {
block.expression?.visit(this);
}
@@ -89,6 +89,16 @@ export enum OpKind {
*/
Conditional,
/**
* Create a boundary creation instruction op.
*/
BoundaryCreate,
/**
* An op to validate and handle errors in a template.
*/
Boundary,
/**
* An operation to re-enable binding, after it was previously disabled.
*/
@@ -375,6 +385,14 @@ export enum ExpressionKind {
*/
ContextLetReference,
/**
* An expression that evaluates to the runtime state object (`LBoundary`) of an error boundary.
* This allows the generated code to access the boundary's caught error when evaluating
* `@error` block conditions.
* During the `resolve_boundaries` phase, this is replaced by a variable read of the stored boundary state.
*/
BoundaryState,
/**
* Runtime operation to snapshot the current view context.
*/
@@ -509,6 +527,12 @@ export enum SemanticVariableKind {
* An alias generated by a special embedded view type (e.g. a `@for` block).
*/
Alias,
/**
* A variable that holds the runtime state (`LBoundary`) of an error boundary.
* It is typically populated by calling `ɵɵgetBoundary()` during the update phase.
*/
BoundaryState,
}
/**
@@ -32,6 +32,7 @@ export type Expression =
| LexicalReadExpr
| ReferenceExpr
| ForeignContentExpr
| BoundaryStateExpr
| ContextExpr
| NextContextExpr
| GetCurrentViewExpr
@@ -191,6 +192,36 @@ export class ForeignContentExpr extends ExpressionBase {
}
}
/**
* Read of a boundary state.
*/
export class BoundaryStateExpr extends ExpressionBase {
override readonly kind = ExpressionKind.BoundaryState;
name: string | null = null;
constructor(readonly xref: XrefId) {
super();
}
override visitExpression(): void {}
override isEquivalent(other: o.Expression): boolean {
return other instanceof BoundaryStateExpr && other.xref === this.xref;
}
override isConstant(): boolean {
return false;
}
override transformInternalExpressions(): void {}
override clone(): BoundaryStateExpr {
const b = new BoundaryStateExpr(this.xref);
b.name = this.name;
return b;
}
}
export class StoreLetExpr
extends ExpressionBase
implements ConsumesVarsTrait, DependsOnSlotContextOpTrait
@@ -1245,6 +1276,18 @@ export function transformExpressionsInOp(
if (op.contextValue !== null) {
op.contextValue = transformExpressionsInExpression(op.contextValue, transform, flags);
}
break;
case OpKind.Boundary:
for (const condition of op.conditions) {
if (condition.expr === null) {
continue;
}
condition.expr = transformExpressionsInExpression(condition.expr, transform, flags);
}
if (op.processed !== null) {
op.processed = transformExpressionsInExpression(op.processed, transform, flags);
}
break;
case OpKind.Animation:
case OpKind.AnimationListener:
@@ -1345,6 +1388,7 @@ export function transformExpressionsInOp(
case OpKind.ConditionalBranchCreate:
case OpKind.Control:
case OpKind.ControlCreate:
case OpKind.BoundaryCreate:
// These operations contain no expressions.
break;
default:
@@ -81,7 +81,8 @@ export type CreateOp =
| AnimationStringOp
| AnimationOp
| SourceLocationOp
| ControlCreateOp;
| ControlCreateOp
| BoundaryCreateOp;
/**
* An operation representing the creation of an element or container.
@@ -94,7 +95,8 @@ export type ElementOrContainerOps =
| TemplateOp
| RepeaterCreateOp
| ConditionalCreateOp
| ConditionalBranchCreateOp;
| ConditionalBranchCreateOp
| BoundaryCreateOp;
/**
* The set of OpKinds that represent the creation of an element or container
@@ -108,6 +110,7 @@ const elementContainerOpKinds = new Set([
OpKind.RepeaterCreate,
OpKind.ConditionalCreate,
OpKind.ConditionalBranchCreate,
OpKind.BoundaryCreate,
]);
/**
@@ -186,7 +189,8 @@ export interface ElementOpBase extends ElementOrContainerOpBase {
| OpKind.Template
| OpKind.RepeaterCreate
| OpKind.ConditionalCreate
| OpKind.ConditionalBranchCreate;
| OpKind.ConditionalBranchCreate
| OpKind.BoundaryCreate;
/**
* The HTML tag name for this element.
@@ -531,6 +535,68 @@ export function createConditionalBranchCreateOp(
};
}
/**
* An op that creates a boundary block.
*/
export interface BoundaryCreateOp extends ElementOpBase {
kind: OpKind.BoundaryCreate;
templateKind: TemplateKind;
/**
* The number of declaration slots used by this template, or `null` if slots have not yet been
* assigned.
*/
decls: number | null;
/**
* The number of binding variable slots used by this template, or `null` if binding variables have
* not yet been counted.
*/
vars: number | null;
/**
* Suffix to add to the name of the generated template function.
*/
functionNameSuffix: string;
/**
* The i18n placeholder data associated with this template.
*/
i18nPlaceholder?: i18n.TagPlaceholder | i18n.BlockPlaceholder;
}
export function createBoundaryCreateOp(
xref: XrefId,
templateKind: TemplateKind,
tag: string | null,
functionNameSuffix: string,
namespace: Namespace,
i18nPlaceholder: i18n.TagPlaceholder | i18n.BlockPlaceholder | undefined,
startSourceSpan: ParseSourceSpan,
wholeSourceSpan: ParseSourceSpan,
): BoundaryCreateOp {
return {
kind: OpKind.BoundaryCreate,
xref,
templateKind,
attributes: null,
tag,
handle: new SlotHandle(),
functionNameSuffix,
decls: null,
vars: null,
localRefs: [],
nonBindable: false,
namespace,
i18nPlaceholder,
startSourceSpan,
wholeSourceSpan,
...TRAIT_CONSUMES_SLOT,
...NEW_OP,
};
}
/**
* An op that creates a repeater (e.g. a for loop).
*/
@@ -57,7 +57,8 @@ export type UpdateOp =
| DeferWhenOp
| AnimationBindingOp
| StoreLetOp
| ControlOp;
| ControlOp
| BoundaryOp;
/**
* A logical operation to perform string interpolation on a text node.
@@ -719,6 +720,65 @@ export function createConditionalOp(
};
}
/**
* A logical operation representing a boundary expression in the update IR.
*/
export interface BoundaryOp extends Op<UpdateOp>, DependsOnSlotContextOpTrait, ConsumesVarsTrait {
kind: OpKind.Boundary;
/**
* The insertion point, which is the BoundaryCreate op belonging to this condition.
*/
target: XrefId;
/**
* The slot handle of the boundary block.
*/
targetSlot: SlotHandle;
/**
* The Xref of the primary branch view.
*/
primaryTarget: XrefId;
/**
* Each possible error fallback view that could be displayed.
*/
conditions: Array<ConditionalCaseExpr>;
/**
* After processing, this will be a single collapsed expression evaluating the error
* against fallback triggers to determine the branch index to render.
*/
processed: o.Expression | null;
sourceSpan: ParseSourceSpan;
}
/**
* Create a Boundary updater op.
*/
export function createBoundaryOp(
target: XrefId,
targetSlot: SlotHandle,
primaryTarget: XrefId,
conditions: Array<ConditionalCaseExpr>,
sourceSpan: ParseSourceSpan,
): BoundaryOp {
return {
kind: OpKind.Boundary,
target,
targetSlot,
primaryTarget,
conditions,
processed: null,
sourceSpan,
...NEW_OP,
...TRAIT_DEPENDS_ON_SLOT_CONTEXT,
...TRAIT_CONSUMES_VARS,
};
}
export interface RepeaterOp extends Op<UpdateOp>, DependsOnSlotContextOpTrait {
kind: OpKind.Repeater;
@@ -14,10 +14,7 @@ import type {XrefId} from './operations';
* Union type for the different kinds of variables.
*/
export type SemanticVariable =
| ContextVariable
| IdentifierVariable
| SavedViewVariable
| AliasVariable;
ContextVariable | IdentifierVariable | SavedViewVariable | AliasVariable | BoundaryStateVariable;
export interface SemanticVariableBase {
kind: SemanticVariableKind;
@@ -84,3 +81,11 @@ export interface AliasVariable extends SemanticVariableBase {
identifier: string;
expression: o.Expression;
}
/**
* A variable that holds the state of a boundary.
*/
export interface BoundaryStateVariable extends SemanticVariableBase {
kind: SemanticVariableKind.BoundaryState;
boundaryXref: XrefId;
}
@@ -29,6 +29,7 @@ import {specializeBindings} from './phases/binding_specialization';
import {chain} from './phases/chaining';
import {collapseSingletonInterpolations} from './phases/collapse_singleton_interpolations';
import {generateConditionalExpressions} from './phases/conditionals';
import {generateBoundaryConditions} from './phases/boundary_conditions';
import {collectElementConsts} from './phases/const_collection';
import {specializeControlProperties} from './phases/control_directives';
import {convertAnimations} from './phases/convert_animations';
@@ -70,6 +71,7 @@ import {removeEmptyBindings} from './phases/remove_empty_bindings';
import {removeI18nContexts} from './phases/remove_i18n_contexts';
import {removeIllegalLetReferences} from './phases/remove_illegal_let_references';
import {removeUnusedI18nAttributesOps} from './phases/remove_unused_i18n_attrs';
import {resolveBoundaries} from './phases/resolve_boundaries';
import {resolveContexts} from './phases/resolve_contexts';
import {resolveDeferDepsFns} from './phases/resolve_defer_deps_fns';
import {resolveDollarEvent} from './phases/resolve_dollar_event';
@@ -127,6 +129,7 @@ const phases: Phase[] = [
{kind: Kind.Both, fn: collapseSingletonInterpolations},
{kind: Kind.Both, fn: orderOps},
{kind: Kind.Tmpl, fn: generateConditionalExpressions},
{kind: Kind.Tmpl, fn: generateBoundaryConditions},
{kind: Kind.Tmpl, fn: createPipes},
{kind: Kind.Tmpl, fn: configureDeferInstructions},
{kind: Kind.Tmpl, fn: insertIncrementalHydrationRuntime},
@@ -136,6 +139,7 @@ const phases: Phase[] = [
{kind: Kind.Tmpl, fn: generateProjectionDefs},
{kind: Kind.Tmpl, fn: generateLocalLetReferences},
{kind: Kind.Tmpl, fn: generateVariables},
{kind: Kind.Tmpl, fn: resolveBoundaries},
{kind: Kind.Tmpl, fn: saveAndRestoreView},
{kind: Kind.Both, fn: deleteAnyCasts},
{kind: Kind.Both, fn: removeSafeNavigationMigration},
@@ -306,6 +306,8 @@ function ingestNodes(unit: ViewCompilationUnit, template: t.Node[]): void {
ingestForBlock(unit, node);
} else if (node instanceof t.LetDeclaration) {
ingestLetDeclaration(unit, node);
} else if (node instanceof t.BoundaryBlock) {
ingestBoundaryBlock(unit, node);
} else if (node instanceof t.Component) {
// TODO(crisbeto): account for selectorless nodes.
} else {
@@ -670,6 +672,102 @@ function ingestIfBlock(unit: ViewCompilationUnit, ifBlock: t.IfBlock): void {
unit.update.push(ir.createConditionalOp(firstXref!, null, conditions, ifBlock.sourceSpan));
}
/**
* Ingest an `@boundary` block into the given `ViewCompilation`.
*/
function ingestBoundaryBlock(unit: ViewCompilationUnit, boundaryBlock: t.BoundaryBlock): void {
// 1. Process primary block first to get its view xref for BoundaryCreateOp
const primaryView = unit.job.allocateView(unit.xref);
const primaryTagName = ingestControlFlowInsertionPoint(unit, primaryView.xref, boundaryBlock);
// Create BoundaryCreateOp for the container itself, using the primary view xref!
const createOp = ir.createBoundaryCreateOp(
unit.job.allocateXrefId(),
ir.TemplateKind.Block,
primaryTagName,
'Boundary',
ir.Namespace.HTML,
undefined,
boundaryBlock.startSourceSpan,
boundaryBlock.sourceSpan,
);
unit.create.push(createOp);
const primaryCreateOp = ir.createConditionalBranchCreateOp(
primaryView.xref,
ir.TemplateKind.Block,
primaryTagName,
'Primary',
ir.Namespace.HTML,
undefined,
boundaryBlock.startSourceSpan,
boundaryBlock.sourceSpan,
);
unit.create.push(primaryCreateOp);
let conditions: Array<ir.ConditionalCaseExpr> = [];
// 2. Process @error blocks (fallbacks)
for (const errorBlock of boundaryBlock.errorBlocks) {
const errorView = unit.job.allocateView(unit.xref);
// Create branch creation operation
const branchCreateOp = ir.createConditionalBranchCreateOp(
errorView.xref,
ir.TemplateKind.Block,
tagName,
'Error',
ir.Namespace.HTML,
undefined,
errorBlock.startSourceSpan,
errorBlock.sourceSpan,
);
unit.create.push(branchCreateOp);
// Expression case
const caseExpr = errorBlock.expression
? convertAst(errorBlock.expression, unit.job, null)
: null;
const conditionalCaseExpr = new ir.ConditionalCaseExpr(
caseExpr,
branchCreateOp.xref,
branchCreateOp.handle,
null,
);
conditions.push(conditionalCaseExpr);
for (const variable of errorBlock.contextVariables) {
errorView.aliases.add({
kind: ir.SemanticVariableKind.Alias,
name: null,
identifier: variable.name,
expression: new o.ReadPropExpr(new ir.ContextExpr(errorView.xref), variable.value),
});
}
ingestNodes(errorView, errorBlock.children);
}
const primaryCaseExpr = new ir.ConditionalCaseExpr(
null,
primaryCreateOp.xref,
primaryCreateOp.handle,
null,
);
conditions.push(primaryCaseExpr);
ingestNodes(primaryView, boundaryBlock.children);
unit.update.push(
ir.createBoundaryOp(
createOp.xref,
createOp.handle,
primaryCreateOp.xref,
primaryCaseExpr,
conditions,
boundaryBlock.sourceSpan,
),
);
}
/**
* Ingest an `@switch` block into the given `ViewCompilation`.
*/
@@ -1982,7 +2080,13 @@ function convertSourceSpan(
function ingestControlFlowInsertionPoint(
unit: ViewCompilationUnit,
xref: ir.XrefId,
node: t.IfBlockBranch | t.SwitchBlockCaseGroup | t.ForLoopBlock | t.ForLoopBlockEmpty,
node:
| t.IfBlockBranch
| t.SwitchBlockCaseGroup
| t.ForLoopBlock
| t.ForLoopBlockEmpty
| t.BoundaryBlock
| t.BoundaryErrorBlock,
): string | null {
let root: t.Element | t.Template | null = null;
@@ -511,6 +511,23 @@ export function conditionalBranchCreate(
return call(Identifiers.conditionalBranchCreate, args, sourceSpan);
}
export function boundaryCreate(slot: number, sourceSpan: ParseSourceSpan): ir.CreateOp {
return call(Identifiers.boundaryCreate, [o.literal(slot)], sourceSpan);
}
export function getBoundary(slot: o.Expression): o.Expression {
return o.importExpr(Identifiers.getBoundary).callFn([slot]);
}
export function boundary(
slot: o.Expression,
processedExpr: o.Expression,
primarySlot: o.Expression,
sourceSpan: ParseSourceSpan | null,
): ir.UpdateOp {
const args = [slot, processedExpr, primarySlot];
return call(Identifiers.boundaryUpdate, args, sourceSpan);
}
export function repeaterCreate(
slot: number,
viewFnName: string,
@@ -0,0 +1,86 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import type {ComponentCompilationJob} from '../compilation';
/**
* Resolves conditions for boundary operations, generating an expression that
* determines which branch to render based on the boundary state.
*/
export function generateBoundaryConditions(job: ComponentCompilationJob): void {
for (const unit of job.units) {
for (const op of unit.update) {
if (op.kind === ir.OpKind.Boundary) {
const boundaryXref = op.target;
// We generate an expression like: `bnd_0.error === null ? primarySlot : errorSlot`
const boundaryStateExpr = new ir.BoundaryStateExpr(boundaryXref);
const errorProp = new o.ReadPropExpr(boundaryStateExpr, 'error');
const condition = new o.BinaryOperatorExpr(
o.BinaryOperator.Identical,
errorProp,
o.NULL_EXPR,
);
const errorBranches = op.conditions;
const fallbackBranch = errorBranches.find((c) => c.expr === null);
let errorResultExpr: o.Expression = fallbackBranch
? new ir.SlotLiteralExpr(fallbackBranch.targetSlot)
: o.literal(-1);
// Iterate in reverse order over error branches WITH conditions
const conditionalBranches = errorBranches.filter((c) => c.expr !== null);
for (let i = conditionalBranches.length - 1; i >= 0; i--) {
const branch = conditionalBranches[i];
let conditionExpr = branch.expr!;
// Find the corresponding BoundaryErrorCreateOp to check for context variables
const errorCreateOp = Array.from(unit.create).find(
(o): o is ir.BoundaryErrorCreateOp =>
o.kind === ir.OpKind.BoundaryErrorCreate && o.xref === branch.target,
);
if (errorCreateOp !== undefined) {
const errorVarNames = errorCreateOp.contextVariables
.filter((v: any) => v.value === '$error')
.map((v: any) => v.name);
if (errorVarNames.length > 0) {
conditionExpr = ir.transformExpressionsInExpression(
conditionExpr,
(expr) => {
if (expr instanceof ir.LexicalReadExpr && errorVarNames.includes(expr.name)) {
return errorProp;
}
return expr;
},
ir.VisitorContextFlag.None,
);
}
}
errorResultExpr = new o.ConditionalExpr(
conditionExpr,
new ir.SlotLiteralExpr(branch.targetSlot),
errorResultExpr,
);
}
op.processed = new o.ConditionalExpr(
condition,
new ir.SlotLiteralExpr(op.guarded.targetSlot),
errorResultExpr,
);
}
}
}
}
@@ -16,7 +16,7 @@ import {ComponentCompilationJob} from '../compilation';
export function generateConditionalExpressions(job: ComponentCompilationJob): void {
for (const unit of job.units) {
for (const op of unit.ops()) {
if (op.kind !== ir.OpKind.Conditional) {
if (op.kind !== ir.OpKind.Conditional && op.kind !== ir.OpKind.Boundary) {
continue;
}
@@ -33,7 +33,7 @@ export function generateConditionalExpressions(job: ComponentCompilationJob): vo
}
// Switch expressions assign their main test to a temporary, to avoid re-executing it.
let tmp = op.test == null ? null : new ir.AssignTemporaryExpr(op.test, job.allocateXrefId());
let tmp = op.test != null ? new ir.AssignTemporaryExpr(op.test, job.allocateXrefId()) : null;
let caseExpressionTemporaryXref: ir.XrefId | null = null;
// For each remaining condition, test whether the temporary satifies the check. (If no temp is
@@ -8,6 +8,7 @@
import * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import * as ng from '../instruction';
import type {ComponentCompilationJob, ViewCompilationUnit} from '../compilation';
@@ -98,6 +99,8 @@ interface Scope {
aliases: Set<ir.AliasVariable>;
boundaryVariables: Map<ir.XrefId, {variable: ir.SemanticVariable; handle: ir.SlotHandle}>;
/**
* Local references collected from elements within the view.
*/
@@ -173,6 +176,7 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
aliases: view.aliases,
references: [],
letDeclarations: [],
boundaryVariables: new Map<ir.XrefId, {variable: ir.SemanticVariable; handle: ir.SlotHandle}>(),
parent,
};
@@ -212,6 +216,17 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
}
break;
case ir.OpKind.BoundaryCreate:
scope.boundaryVariables.set(op.xref, {
variable: {
kind: ir.SemanticVariableKind.BoundaryState,
name: null,
boundaryXref: op.xref,
},
handle: op.handle,
});
break;
case ir.OpKind.DeclareLet:
scope.letDeclarations.push({
targetId: op.xref,
@@ -292,6 +307,18 @@ function generateVariablesInScopeForView<OpT extends ir.Op<OpT>>(
);
}
// Add variables for all boundaries declared in this scope.
for (const [xref, {variable, handle}] of scope.boundaryVariables) {
newOps.push(
ir.createVariableOp(
view.job.allocateXrefId(),
variable,
ng.getBoundary(new ir.SlotLiteralExpr(handle)),
ir.VariableFlags.None,
),
);
}
// Add variables for all local references declared for elements in this scope.
for (const ref of scope.references) {
newOps.push(
@@ -191,6 +191,9 @@ function getVariableName(variable: ir.SemanticVariable, state: {index: number}):
const compatPrefix = variable.identifier === CONTEXT_NAME ? 'i' : '';
variable.name = `${variable.identifier}_${compatPrefix}r${++state.index}`;
break;
case ir.SemanticVariableKind.BoundaryState:
variable.name = `bnd_r${++state.index}`;
break;
default:
// TODO: Prefix increment for compatibility only.
variable.name = `_r${++state.index}`;
@@ -7,9 +7,9 @@
*/
import * as o from '../../../../output/output_ast';
import {CONTEXT_NAME} from '../../../../render3/view/util';
import {isUnsafeObjectKey} from '../../../../render3/util';
import {Identifiers} from '../../../../render3/r3_identifiers';
import {isUnsafeObjectKey} from '../../../../render3/util';
import {CONTEXT_NAME} from '../../../../render3/view/util';
import * as ir from '../../ir';
import {
TemplateCompilationMode,
@@ -557,6 +557,17 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
),
);
break;
case ir.OpKind.BoundaryCreate:
if (!(unit instanceof ViewCompilationUnit)) {
throw new Error(`AssertionError: must be compiling a component`);
}
if (Array.isArray(op.localRefs) && op.localRefs.length > 0) {
throw new Error(
`AssertionError: local refs array should have been extracted into a constant`,
);
}
ir.OpList.replace(op, ng.boundaryCreate(op.handle.slot!, op.startSourceSpan));
break;
case ir.OpKind.RepeaterCreate:
if (op.handle.slot === null) {
throw new Error('No slot was assigned for repeater instruction');
@@ -737,6 +748,28 @@ function reifyUpdateOperations(unit: CompilationUnit, ops: ir.OpList<ir.UpdateOp
}
ir.OpList.replace(op, ng.conditional(op.processed, op.contextValue, op.sourceSpan));
break;
case ir.OpKind.Boundary:
if (op.processed === null) {
throw new Error(`Boundary test was not set.`);
}
const boundarySlot = op.targetSlot.slot;
if (boundarySlot === null) {
throw new Error(`AssertionError: Boundary target slot not found`);
}
const primarySlot = op.guarded.targetSlot.slot;
if (primarySlot === null) {
throw new Error(`AssertionError: Primary slot not found for boundary`);
}
ir.OpList.replace(
op,
ng.boundary(
o.literal(boundarySlot),
op.processed!,
o.literal(primarySlot),
op.sourceSpan,
),
);
break;
case ir.OpKind.Repeater:
ir.OpList.replace(op, ng.repeater(op.collection, op.sourceSpan));
break;
@@ -0,0 +1,55 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import * as o from '../../../../output/output_ast';
import * as ir from '../../ir';
import type {CompilationJob, CompilationUnit} from '../compilation';
/**
* Resolves `BoundaryStateExpr` to `ReadVariableExpr` by mapping the boundary `XrefId`
* to the `XrefId` of the `VariableOp` that retrieves the boundary state.
*/
export function resolveBoundaries(job: CompilationJob): void {
for (const unit of job.units) {
processUnit(unit);
}
}
function processUnit(unit: CompilationUnit): void {
// Map from boundary create op xref to variable op xref.
const boundaryToVariableMap = new Map<ir.XrefId, ir.XrefId>();
// First pass: find all VariableOps that hold boundary state and build the map.
for (const op of unit.ops()) {
if (
op.kind === ir.OpKind.Variable &&
op.variable.kind === ir.SemanticVariableKind.BoundaryState
) {
const variable = op.variable as ir.BoundaryStateVariable;
boundaryToVariableMap.set(variable.boundaryXref, op.xref);
}
}
// Second pass: resolve BoundaryStateExpr to ReadVariableExpr.
for (const op of unit.ops()) {
ir.transformExpressionsInOp(
op,
(expr) => {
if (expr instanceof ir.BoundaryStateExpr) {
const variableXref = boundaryToVariableMap.get(expr.xref);
if (variableXref === undefined) {
throw new Error(`Could not find variable for boundary ${expr.xref}`);
}
return new ir.ReadVariableExpr(variableXref);
}
return expr;
},
ir.VisitorContextFlag.None,
);
}
}
@@ -160,6 +160,7 @@ function varsUsedByOp(op: (ir.CreateOp | ir.UpdateOp) & ir.ConsumesVarsTrait): n
return op.interpolation.expressions.length;
case ir.OpKind.I18nExpression:
case ir.OpKind.Conditional:
case ir.OpKind.Boundary:
case ir.OpKind.DeferWhen:
case ir.OpKind.StoreLet:
return 1;
@@ -13,6 +13,7 @@ import {
BoundText,
Component,
Content,
BoundaryErrorBlock,
DeferredBlock,
DeferredBlockTriggers,
Directive,
@@ -225,6 +226,22 @@ export class Scope {
),
);
}
} else if (scopedNode instanceof BoundaryErrorBlock) {
for (const variable of scopedNode.contextVariables) {
let typeExpr: TcbExpr;
if (variable.value === '$error') {
typeExpr = new TcbExpr(`(err as Error)`);
} else if (variable.value === '$retry') {
typeExpr = new TcbExpr(`(() => {})`);
} else {
throw new Error(`Unrecognized context variable ${variable.value}`);
}
Scope.registerVariable(
scope,
variable,
new TcbBlockVariableOp(tcb, scope, typeExpr, variable),
);
}
} else if (scopedNode instanceof ForLoopBlock) {
// Register the variable for the loop so it can be resolved by
// children. It'll be declared once the loop is created.
@@ -219,6 +219,25 @@ class R3AstSourceSpans implements t.Visitor<void> {
this.visitAll([block.children]);
}
visitBoundaryBlock(block: t.BoundaryBlock): void {
this.result.push([
'BoundaryBlock',
humanizeSpan(block.sourceSpan),
humanizeSpan(block.startSourceSpan),
humanizeSpan(block.endSourceSpan),
]);
this.visitAll([block.children, block.errorBlocks]);
}
visitBoundaryErrorBlock(block: t.BoundaryErrorBlock): void {
this.result.push([
'BoundaryErrorBlock',
humanizeSpan(block.sourceSpan),
humanizeSpan(block.startSourceSpan),
]);
this.visitAll([block.children]);
}
visitDeferredTrigger(trigger: t.DeferredTrigger): void {
let name: string;
@@ -153,6 +153,16 @@ class R3AstHumanizer implements t.Visitor<void> {
this.visitAll(toVisit);
}
visitBoundaryBlock(block: t.BoundaryBlock): void {
this.result.push(['BoundaryBlock']);
this.visitAll([block.children, block.errorBlocks]);
}
visitBoundaryErrorBlock(block: t.BoundaryErrorBlock): void {
this.result.push(['BoundaryErrorBlock', block.expression ? unparse(block.expression) : null]);
this.visitAll([block.contextVariables, block.children]);
}
visitDeferredTrigger(trigger: t.DeferredTrigger): void {
if (trigger instanceof t.BoundDeferredTrigger) {
this.result.push(['BoundDeferredTrigger', unparse(trigger.value)]);
@@ -267,6 +277,35 @@ describe('R3 template transform', () => {
});
});
describe('Boundary blocks', () => {
it('should parse @boundary blocks with multiple @error blocks', () => {
expectFromHtml(`
@boundary {
<div>Content</div>
} @error (when err instanceof SpecificError) {
<div>Specific Error</div>
} @error (let err) {
<div>General Error {{ err }}</div>
}
`).toEqual([
['BoundaryBlock'],
['Element', 'div'],
['Text', 'Content'],
['BoundaryErrorBlock', 'err instanceof SpecificError'],
['Variable', '$error', '$error'],
['Variable', '$reset', '$reset'],
['Element', 'div'],
['Text', 'Specific Error'],
['BoundaryErrorBlock', null],
['Variable', '$error', '$error'],
['Variable', '$reset', '$reset'],
['Variable', 'err', '$error'],
['Element', 'div'],
['BoundText', 'General Error {{ err }}'],
]);
});
});
describe('Nodes without binding', () => {
it('should parse incomplete tags terminated by EOF', () => {
expectFromHtml('<a', true /* ignoreError */).toEqual([['Element', 'a']]);
@@ -1432,7 +1471,7 @@ describe('R3 template transform', () => {
/@loading block can only be used after an @defer block/,
);
expect(() => parse('@error {error}')).toThrowError(
/@error block can only be used after an @defer block/,
/@error block can only be used after an @defer or @boundary block/,
);
});
@@ -243,6 +243,16 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Visit
t.visitAll(this, block.children);
}
visitBoundaryBlock(block: t.BoundaryBlock) {
t.visitAll(this, block.children);
t.visitAll(this, block.errorBlocks);
}
visitBoundaryErrorBlock(block: t.BoundaryErrorBlock) {
block.expression?.visit(this);
t.visitAll(this, block.children);
}
visitLetDeclaration(decl: t.LetDeclaration) {
decl.value.visit(this);
}
@@ -38,6 +38,8 @@ import type {
TmplAstUnknownBlock,
TmplAstVariable,
TmplAstContentBlock,
TmplAstBoundaryBlock,
TmplAstBoundaryErrorBlock,
} from '@angular/compiler';
/**
@@ -91,6 +93,8 @@ export class TemplateAstVisitor implements TmplAstRecursiveVisitor {
visitDirective(directive: TmplAstDirective): void {}
visitSwitchExhaustiveCheck(block: TmplAstSwitchExhaustiveCheck): void {}
visitContentBlock(block: TmplAstContentBlock): void {}
visitBoundaryBlock(block: TmplAstBoundaryBlock): void {}
visitBoundaryErrorBlock(block: TmplAstBoundaryErrorBlock): void {}
/**
* Visits all the provided nodes in order using this Visitor's visit methods.
+1 -1
View File
@@ -67,7 +67,7 @@ export {
export {IdleService, provideIdleServiceWith} from './defer/idle_service';
export * from './di';
export {DOCUMENT} from './document';
export {ErrorHandler, provideBrowserGlobalErrorListeners} from './error_handler';
export {ErrorDetails, ErrorHandler, provideBrowserGlobalErrorListeners} from './error_handler';
export {EventEmitter} from './event_emitter';
export {
DEFAULT_CURRENCY_CODE,
@@ -11,12 +11,12 @@
//
// no code actually imports these symbols from the @angular/core entry point
export {
type DebugSignalGraph as ɵDebugSignalGraph,
type DebugSignalGraphEdge as ɵDebugSignalGraphEdge,
type DebugSignalGraphNode as ɵDebugSignalGraphNode,
Framework as ɵFramework,
Profiler as ɵProfiler,
ProfilerEvent as ɵProfilerEvent,
type DebugSignalGraph as ɵDebugSignalGraph,
type DebugSignalGraphEdge as ɵDebugSignalGraphEdge,
type DebugSignalGraphNode as ɵDebugSignalGraphNode,
} from '../primitives/devtools';
export {compileNgModuleFactory as ɵcompileNgModuleFactory} from './application/application_ngmodule_factory_compiler';
export {injectChangeDetectorRef as ɵinjectChangeDetectorRef} from './change_detection/change_detector_ref';
@@ -26,6 +26,7 @@ export {
NG_INJ_DEF as ɵNG_INJ_DEF,
NG_PROV_DEF as ɵNG_PROV_DEF,
} from './di/interface/defs';
export {ɵɵenableIncrementalHydrationRuntime} from './hydration/incremental_runtime';
export {
setAllowDuplicateNgModuleIdsForTest as ɵsetAllowDuplicateNgModuleIdsForTest,
registerNgModuleType as ɵɵregisterNgModuleType,
@@ -36,10 +37,9 @@ export {
} from './metadata/ng_module_def';
export {AfterRenderManager as ɵAfterRenderManager} from './render3/after_render/manager';
export {inferTagNameFromDefinition as ɵinferTagNameFromDefinition} from './render3/component_ref';
export {ɵɵenableIncrementalHydrationRuntime} from './hydration/incremental_runtime';
export {getLContext as ɵgetLContext} from './render3/context_discovery';
export {depsTracker as ɵdepsTracker} from './render3/deps_tracker/deps_tracker';
export {getComponentInstanceDeepLinkId as ɵgetComponentInstanceDeepLinkId} from './render3/debug/chrome_dev_tools_performance';
export {depsTracker as ɵdepsTracker} from './render3/deps_tracker/deps_tracker';
export {
NG_COMP_DEF as ɵNG_COMP_DEF,
NG_DIR_DEF as ɵNG_DIR_DEF,
@@ -95,6 +95,8 @@ export {
ɵɵarrowFunction,
ɵɵattachSourceLocations,
ɵɵattribute,
ɵɵboundaryCreate,
ɵɵboundaryUpdate,
ɵɵclassMap,
ɵɵclassProp,
ɵɵComponentDeclaration,
@@ -154,12 +156,13 @@ export {
ɵɵelementContainerStart,
ɵɵelementEnd,
ɵɵelementStart,
ɵɵforeignComponent,
ɵɵforeignContent,
ɵɵforeignContentFn,
ɵɵenableBindings,
ɵɵExternalStylesFeature,
ɵɵFactoryDeclaration,
ɵɵforeignComponent,
ɵɵforeignContent,
ɵɵforeignContentFn,
ɵɵgetBoundary,
ɵɵgetComponentDepsFactory,
ɵɵgetCurrentView,
ɵɵgetInheritedFactory,
@@ -280,10 +283,10 @@ export {
ɵɵngDeclareDirective,
ɵɵngDeclareFactory,
ɵɵngDeclareInjectable,
ɵɵngDeclareService,
ɵɵngDeclareInjector,
ɵɵngDeclareNgModule,
ɵɵngDeclarePipe,
ɵɵngDeclareService,
} from './render3/jit/partial';
export {compilePipe as ɵcompilePipe} from './render3/jit/pipe';
export {isNgModule as ɵisNgModule} from './render3/jit/util';
+3
View File
@@ -120,6 +120,9 @@ export {
ɵɵconditional,
ɵɵconditionalCreate,
ɵɵconditionalBranchCreate,
ɵɵboundaryCreate,
ɵɵboundaryUpdate,
ɵɵgetBoundary,
ɵɵdefer,
ɵɵdeferWhen,
ɵɵdeferOnIdle,
@@ -34,6 +34,7 @@ export * from './change_detection';
export * from './component_instance';
export * from './control';
export * from './control_flow';
export * from './boundary';
export * from './di';
export * from './di_attr';
export * from './element';
@@ -9,7 +9,7 @@
import {setActiveConsumer} from '../../../primitives/signals';
import {ErrorDetails, ErrorHandler, encapsulateBoundaryError} from '../../error_handler';
import {findAndReconcileMatchingDehydratedViews} from '../../hydration/views';
import {Type} from '../../interface/type';
import {Type, Writable} from '../../interface/type';
import {performanceMarkFeature} from '../../util/performance';
import {bindingUpdated} from '../bindings';
import {
@@ -21,15 +21,19 @@ import {
ON_ERROR,
TVIEW,
} from '../interfaces/view';
import {ComponentTemplate} from '../interfaces/definition';
import {LocalRefExtractor} from '../interfaces/node';
import {getLView, nextBindingIndex} from '../state';
import {NO_CHANGE} from '../tokens';
import {markViewForRefresh} from '../util/view_utils';
import {addLViewToLContainer, removeLViewFromLContainer} from '../view/container';
import {createAndRenderEmbeddedLView, shouldAddViewToDom} from '../view_manipulation';
import {getExistingTNode, getLContainer} from './control_flow';
import {createControlFlowBranch, getExistingTNode, getLContainer} from './control_flow';
/**
* Error thrown when an error falls through an @boundary without matching any @error block.
*
* @publicApi 22.2
*/
export class BoundaryError extends Error {
constructor(message: string, options?: {cause?: unknown}) {
@@ -116,13 +120,13 @@ export function ɵɵboundaryUpdate(
const context =
matchingTemplateIndex === primaryTemplateIndex
? undefined
: {$error: boundary.error, $retry: () => boundary.reset()};
: {$error: boundary.error, $reset: () => boundary.reset()};
embeddedLView = createAndRenderEmbeddedLView(hostLView, templateTNode, context, {
dehydratedView,
});
if (matchingTemplateIndex === primaryTemplateIndex) {
embeddedLView[ON_ERROR] = (error: Error, details: any) => {
embeddedLView[ON_ERROR] = (error: Error, details: ErrorDetails) => {
const boundary = hostLView[HEADER_OFFSET + slotIndex] as LBoundary;
boundary.error = error;
@@ -130,7 +134,7 @@ export function ɵɵboundaryUpdate(
if (errorHandler) {
const boundaryComponentView = hostLView[DECLARATION_COMPONENT_VIEW][CONTEXT] as any;
const boundaryType: Type<unknown> = boundaryComponentView.constructor;
details.boundary = {
(details as Writable<ErrorDetails>).boundary = {
type: boundaryType,
reset: () => boundary.reset(),
};
@@ -193,11 +197,9 @@ export function ɵɵboundaryUpdate(
);
} else {
const boundary = hostLView[HEADER_OFFSET + slotIndex] as LBoundary;
if (boundary.error !== null) {
throw new BoundaryError('Unhandled error in @boundary fell through.', {
cause: boundary.error,
});
}
throw new BoundaryError('Unhandled error in @boundary fell through.', {
cause: boundary.error,
});
}
} finally {
setActiveConsumer(prevConsumer);
@@ -16,6 +16,7 @@ import {
setActiveConsumer,
} from '../../../primitives/signals';
import {ProfilerEvent} from '../../../primitives/devtools';
import {encapsulateBoundaryError, ErrorDetails} from '../../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../../errors';
import {Type} from '../../interface/type';
@@ -29,7 +30,7 @@ import {
MOVED_VIEWS,
} from '../interfaces/container';
import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interfaces/definition';
import {isLContainer} from '../interfaces/type_checks';
import {isDestroyed, isLContainer} from '../interfaces/type_checks';
import {
CONTEXT,
DECLARATION_COMPONENT_VIEW,
@@ -45,6 +46,8 @@ import {
TVIEW,
TView,
} from '../interfaces/view';
import {profiler} from '../profiler';
import {executeViewQueryFn, refreshContentQueries} from '../queries/query_execution';
import {
getOrBorrowReactiveLViewConsumer,
getOrCreateTemporaryConsumer,
@@ -52,6 +55,7 @@ import {
ReactiveLViewConsumer,
viewShouldHaveReactiveConsumer,
} from '../reactive_lview_consumer';
import {runEffectsInView} from '../reactivity/view_effect_runner';
import {
CheckNoChangesMode,
enterView,
@@ -76,11 +80,6 @@ import {
viewAttachedToChangeDetector,
} from '../util/view_utils';
import {ProfilerEvent} from '../../../primitives/devtools';
import {isDestroyed} from '../interfaces/type_checks';
import {profiler} from '../profiler';
import {executeViewQueryFn, refreshContentQueries} from '../queries/query_execution';
import {runEffectsInView} from '../reactivity/view_effect_runner';
import {executeTemplate} from './shared';
/**
@@ -391,7 +390,6 @@ export function refreshView<T>(
// If the error handler itself throws, capture the new error and
// continue propagating it up the tree to the next error boundary.
errorToHandle = boundaryError;
handled = false;
}
}
currentLView = currentLView[PARENT];
@@ -42,6 +42,8 @@ import {getConstant, getTNode} from '../util/view_utils';
import {createAndRenderEmbeddedLView, shouldAddViewToDom} from '../view_manipulation';
import {AnimationLViewData} from '../../animation/interfaces';
import {allLeavingAnimations} from '../../animation/longest_animation';
import {removeFromAnimationQueue} from '../../animation/queue';
import {removeDehydratedViews} from '../../hydration/cleanup';
import {
addLViewToLContainer,
@@ -50,8 +52,6 @@ import {
removeLViewFromLContainer,
} from '../view/container';
import {declareNoDirectiveHostTemplate} from './template';
import {removeFromAnimationQueue} from '../../animation/queue';
import {allLeavingAnimations} from '../../animation/longest_animation';
/**
* Creates an LContainer for an ng-template representing a root node
@@ -130,6 +130,32 @@ export function ɵɵconditionalBranchCreate(
localRefExtractor?: LocalRefExtractor,
): typeof ɵɵconditionalBranchCreate {
performanceMarkFeature('NgControlFlow');
createControlFlowBranch(
index,
templateFn,
decls,
vars,
tagName,
attrsIndex,
localRefsIndex,
localRefExtractor,
);
return ɵɵconditionalBranchCreate;
}
/**
* Shared internal function to create a control flow branch (e.g. for @case, @else, @error).
*/
export function createControlFlowBranch(
index: number,
templateFn: ComponentTemplate<any> | null,
decls: number,
vars: number,
tagName?: string | null,
attrsIndex?: number | null,
localRefsIndex?: number | null,
localRefExtractor?: LocalRefExtractor,
) {
const lView = getLView();
const tView = getTView();
const attrs = getConstant<TAttributes>(tView.consts, attrsIndex);
@@ -147,7 +173,6 @@ export function ɵɵconditionalBranchCreate(
localRefsIndex,
localRefExtractor,
);
return ɵɵconditionalBranchCreate;
}
/**
@@ -568,7 +593,7 @@ export function ɵɵrepeater(collection: Iterable<unknown> | undefined | null):
}
}
function getLContainer(lView: LView, index: number): LContainer {
export function getLContainer(lView: LView, index: number): LContainer {
const lContainer = lView[index];
ngDevMode && assertLContainer(lContainer);
@@ -625,7 +650,7 @@ function getExistingLViewFromLContainer<T>(lContainer: LContainer, index: number
return existingLView!;
}
function getExistingTNode(tView: TView, index: number): TNode {
export function getExistingTNode(tView: TView, index: number): TNode {
const tNode = getTNode(tView, index);
ngDevMode && assertTNode(tNode);
@@ -114,6 +114,9 @@ export const angularCoreEnv: {[name: string]: unknown} = (() => ({
'ɵɵconditional': r3.ɵɵconditional,
'ɵɵconditionalCreate': r3.ɵɵconditionalCreate,
'ɵɵconditionalBranchCreate': r3.ɵɵconditionalBranchCreate,
'ɵɵboundaryCreate': r3.ɵɵboundaryCreate,
'ɵɵboundaryUpdate': r3.ɵɵboundaryUpdate,
'ɵɵgetBoundary': r3.ɵɵgetBoundary,
'ɵɵdefer': r3.ɵɵdefer,
'ɵɵdeferWhen': r3.ɵɵdeferWhen,
'ɵɵdeferOnIdle': r3.ɵɵdeferOnIdle,
+1 -9
View File
@@ -29,6 +29,7 @@ import {
import {assertInInjectionContext} from '../di/contextual';
import {Injector} from '../di/injector';
import {inject} from '../di/injector_compatibility';
import {isErrorLike} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
import {CACHE_ACTIVE} from '../hydration/cache';
import {DestroyRef} from '../linker/destroy_ref';
@@ -606,15 +607,6 @@ export function encapsulateResourceError(error: unknown): Error {
return new ResourceWrappedError(error);
}
export function isErrorLike(error: unknown): error is Error {
return (
error instanceof Error ||
(typeof error === 'object' &&
typeof (error as Error).name === 'string' &&
typeof (error as Error).message === 'string')
);
}
export class ResourceValueError extends Error {
constructor(error: Error) {
super(
@@ -1,13 +1,24 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
Component,
EnvironmentInjector,
signal,
ErrorHandler,
Input,
TemplateRef,
ViewChild,
ViewContainerRef,
effect,
signal,
} from '@angular/core';
import {DeferBlockBehavior, TestBed} from '@angular/core/testing';
import {ErrorBoundaryWrappedError, ErrorDetails, ErrorHandler} from '../../src/error_handler';
import {DeferBlockBehavior, DeferBlockState, TestBed} from '@angular/core/testing';
import {ErrorBoundaryWrappedError, ErrorDetails} from '../../src/error_handler';
describe('Error Boundary Runtime Interception', () => {
it('should intercept errors using createComponent onError', () => {
@@ -15,7 +26,6 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '{{ throwError() }}',
standalone: true,
})
class ThrowingComponent {
throwError() {
@@ -25,13 +35,12 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '<ng-container #vc></ng-container>',
standalone: true,
})
class HostComponent {
class Host {
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
}
const fixture = TestBed.createComponent(HostComponent);
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const envInjector = TestBed.inject(EnvironmentInjector);
@@ -43,11 +52,9 @@ describe('Error Boundary Runtime Interception', () => {
},
});
// The inner component is created and attached, but it hasn't run CD yet?
// Actually `createComponent` doesn't run CD by default, we need to call `detectChanges` on the HostComponent.
// `createComponent` doesn't run CD by default, we need to call `detectChanges` on the Host.
expect(() => fixture.detectChanges()).not.toThrow();
expect(interceptedError).toBeDefined();
expect(interceptedError).toBeInstanceOf(Error);
expect(interceptedError!.message).toBe('Component Error');
});
@@ -60,9 +67,8 @@ describe('Error Boundary Runtime Interception', () => {
<ng-template #tpl>{{ throwError() }}</ng-template>
<ng-container #vc></ng-container>
`,
standalone: true,
})
class HostComponent {
class Host {
@ViewChild('tpl', {static: true}) tpl!: TemplateRef<any>;
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
@@ -71,7 +77,7 @@ describe('Error Boundary Runtime Interception', () => {
}
}
const fixture = TestBed.createComponent(HostComponent);
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
fixture.componentInstance.vc.createEmbeddedView(
@@ -95,7 +101,6 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '...',
standalone: true,
})
class ThrowingInitComponent {
ngOnInit() {
@@ -105,13 +110,12 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '<ng-container #vc></ng-container>',
standalone: true,
})
class HostComponent {
class Host {
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
}
const fixture = TestBed.createComponent(HostComponent);
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const envInjector = TestBed.inject(EnvironmentInjector);
@@ -130,12 +134,11 @@ describe('Error Boundary Runtime Interception', () => {
expect(interceptedError!.message).toBe('Init Error');
});
it('should intercept errors thrown during component constructor via createComponent', () => {
it('should NOT intercept errors thrown during component constructor via createComponent', () => {
let interceptedError: Error | null = null;
@Component({
template: '...',
standalone: true,
})
class ThrowingConstructorComponent {
constructor() {
@@ -145,13 +148,12 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '<ng-container #vc></ng-container>',
standalone: true,
})
class HostComponent {
class Host {
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
}
const fixture = TestBed.createComponent(HostComponent);
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const envInjector = TestBed.inject(EnvironmentInjector);
@@ -173,7 +175,6 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '<ng-container #vc></ng-container>',
standalone: true,
})
class MiddleComponent {
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
@@ -181,7 +182,6 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '...',
standalone: true,
})
class ThrowChild {
ngOnInit() {
@@ -191,13 +191,12 @@ describe('Error Boundary Runtime Interception', () => {
@Component({
template: '<ng-container #vc></ng-container>',
standalone: true,
})
class HostComponent {
class Host {
@ViewChild('vc', {read: ViewContainerRef, static: true}) vc!: ViewContainerRef;
}
const fixture = TestBed.createComponent(HostComponent);
const fixture = TestBed.createComponent(Host);
fixture.detectChanges();
const middleRef = fixture.componentInstance.vc.createComponent(MiddleComponent, {
@@ -1002,7 +1001,7 @@ describe('@boundary runtime instructions (JIT)', () => {
@Component({
template: `
@boundary {
<throwing-effect></throwing-effect>
<throwing-effect/>
Main Content
} @error (let err) {
Error: {{err.message}}
@@ -1064,6 +1063,6 @@ describe('@boundary runtime instructions (JIT)', () => {
expect(capturedDetails.declarationType).toBe(Throwing);
expect(capturedDetails.boundary).toBeDefined();
expect(capturedDetails.boundary!.type).toBe(Host);
expect(typeof capturedDetails.boundary!.reset).toBe('function');
expect(typeof capturedDetails.boundary!.retry).toBe('function');
});
});