mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Kristiyan Kostadinov
parent
f6afb807c1
commit
54ed62d240
@@ -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;
|
||||
|
||||
+273
@@ -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>;
|
||||
}
|
||||
|
||||
+75
@@ -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 {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+23
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@boundary {
|
||||
Main Content
|
||||
} @error (let err) {
|
||||
Error: {{err.message}}
|
||||
}
|
||||
`
|
||||
})
|
||||
export class TestComponent {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
@@ -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 {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+36
@@ -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);
|
||||
}
|
||||
}
|
||||
+17
@@ -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 {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -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 {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+27
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@boundary {
|
||||
Main Content
|
||||
} @error {
|
||||
<button (click)="$reset()">Retry</button>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class TestComponent {}
|
||||
+5
@@ -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>;
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
}
|
||||
+12
@@ -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 {}
|
||||
+5
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -152,6 +152,7 @@ const SUPPORTED_BLOCKS = [
|
||||
'@defer',
|
||||
'@placeholder',
|
||||
'@loading',
|
||||
'@boundary',
|
||||
'@error',
|
||||
'@content',
|
||||
] as const;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user