[Components] Relayer + Robust reconnect (#8911)

* [MVC][Components] Prerendering + Robust reconnect
* Relayers prerendering support on a separate package on top of MVC and
  components.
* Implements robust reconects with acknowledgements from the client.
* Improves interactive prerendering with the ability to reconnect to
  prerendered components.
* Removes the need to register components statically when prerendering
  them.
* Removes the need of using an element selector when prerendering an
  interactive component.
* Updates the templates to use the new fallback routing pattern and
  reenables the components test.
* Adds eslint to the Typescript project to help maintain a consistent
  style.
* Adds logging to support better debugging based on the pattern used by
  signalr.
* Fixes exception handling on the server to always report exceptions correctly to the client.
This commit is contained in:
Javier Calvarro Nelson 2019-04-02 19:17:03 +02:00 committed by GitHub
parent 3cc3ab00c9
commit 8499a27c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 3106 additions and 588 deletions

View File

@ -96,6 +96,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Razor" ProjectPath="$(RepositoryRoot)src\Razor\Razor\src\Microsoft.AspNetCore.Razor.csproj" RefProjectPath="$(RepositoryRoot)src\Razor\Razor\ref\Microsoft.AspNetCore.Razor.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Abstractions" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\src\Microsoft.AspNetCore.Mvc.Abstractions.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\ref\Microsoft.AspNetCore.Mvc.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.ApiExplorer" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\src\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\ref\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\ref\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Core" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\ref\Microsoft.AspNetCore.Mvc.Core.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Cors" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\src\Microsoft.AspNetCore.Mvc.Cors.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\ref\Microsoft.AspNetCore.Mvc.Cors.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.DataAnnotations" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\src\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\ref\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" />

View File

@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
{
internal WebAssemblyUriHelper() { }
public static readonly Microsoft.AspNetCore.Blazor.Services.WebAssemblyUriHelper Instance;
protected override void InitializeState() { }
protected override void EnsureInitialized() { }
protected override void NavigateToCore(string uri, bool forceLoad) { }
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string newAbsoluteUri) { }

View File

@ -25,11 +25,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
{
}
/// <summary>
/// Called to initialize BaseURI and current URI before those values are used the first time.
/// Override this method to dynamically calculate those values.
/// </summary>
protected override void InitializeState()
protected override void EnsureInitialized()
{
WebAssemblyJSRuntime.Instance.Invoke<object>(
Interop.EnableNavigationInterception,
@ -40,8 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
// client-side (Mono) use, so it's OK to rely on synchronicity here.
var baseUri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetBaseUri);
var uri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetLocationHref);
SetAbsoluteBaseUri(baseUri);
SetAbsoluteUri(uri);
InitializeState(uri, baseUri);
}
/// <inheritdoc />

View File

@ -452,7 +452,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected override void HandleException(Exception exception)
{
throw new NotImplementedException();
ExceptionDispatchInfo.Capture(exception).Throw();
}
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)

View File

@ -13,13 +13,16 @@
"@aspnet/signalr": "^1.0.0",
"@aspnet/signalr-protocol-msgpack": "^1.0.0",
"@dotnet/jsinterop": "^0.1.1",
"@types/jsdom": "11.0.6",
"@types/jest": "^24.0.6",
"@types/emscripten": "0.0.31",
"@types/jest": "^24.0.6",
"@types/jsdom": "11.0.6",
"@typescript-eslint/eslint-plugin": "^1.5.0",
"@typescript-eslint/parser": "^1.5.0",
"eslint": "^5.16.0",
"jest": "^24.1.0",
"ts-jest": "^24.0.0",
"ts-loader": "^4.4.1",
"typescript": "^2.9.2",
"typescript": "^3.4.0",
"webpack": "^4.12.0",
"webpack-cli": "^3.0.8"
}

View File

@ -0,0 +1,40 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
],
env: {
browser: true,
es6: true,
},
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-use-before-define": [ "off" ],
"no-var": "error",
"prefer-const": "error",
"quotes": ["error", "single", { "avoidEscape": true }],
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"semi-spacing": ["error", { "after": true }],
"spaced-comment": ["error", "always"],
"unicode-bom": ["error", "never"],
"brace-style": ["error", "1tbs"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore"
}],
"comma-style": ["error", "last"],
"comma-spacing": ["error", { "after": true }],
"no-trailing-spaces": ["error"]
},
globals: {
DotNet: "readonly"
}
};

View File

@ -2,15 +2,27 @@ import '@dotnet/jsinterop';
import './GlobalExports';
import * as signalR from '@aspnet/signalr';
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
import { OutOfProcessRenderBatch } from './Rendering/RenderBatch/OutOfProcessRenderBatch';
import { internalFunctions as uriHelperFunctions } from './Services/UriHelper';
import { renderBatch } from './Rendering/Renderer';
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync } from './BootCommon';
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
import RenderQueue from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, ILogger } from './Platform/Logging/ILogger';
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
async function boot() {
const circuitHandlers: CircuitHandler[] = [ new AutoReconnectCircuitHandler() ];
let renderingFailed = false;
async function boot(): Promise<void> {
// For development.
// Simply put a break point here and modify the log level during
// development to get traces.
// In the future we will allow for users to configure this.
const logger = new ConsoleLogger(LogLevel.Error);
logger.log(LogLevel.Information, 'Booting blazor.');
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
window['Blazor'].circuitHandlers = circuitHandlers;
// In the background, start loading the boot config and any embedded resources
@ -18,19 +30,35 @@ async function boot() {
return loadEmbeddedResourcesAsync(bootConfig);
});
const initialConnection = await initializeConnection(circuitHandlers);
const initialConnection = await initializeConnection(circuitHandlers, logger);
const circuits = discoverPrerenderedCircuits(document);
for (let i = 0; i < circuits.length; i++) {
const circuit = circuits[i];
for (let j = 0; j < circuit.components.length; j++) {
const component = circuit.components[j];
component.initialize();
}
}
// Ensure any embedded resources have been loaded before starting the app
await embeddedResourcesPromise;
const circuitId = await initialConnection.invoke<string>(
'StartCircuit',
uriHelperFunctions.getLocationHref(),
uriHelperFunctions.getBaseURI()
);
window['Blazor'].reconnect = async () => {
const reconnection = await initializeConnection(circuitHandlers);
if (!(await reconnection.invoke<Boolean>('ConnectCircuit', circuitId))) {
const circuit = await startCircuit(initialConnection);
if (!circuit) {
logger.log(LogLevel.Information, 'No preregistered components to render.');
}
const reconnect = async (): Promise<boolean> => {
if (renderingFailed) {
// We can't reconnect after a failure, so exit early.
return false;
}
const reconnection = await initializeConnection(circuitHandlers, logger);
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
if (reconnectionFailed(results)) {
return false;
}
@ -38,10 +66,22 @@ async function boot() {
return true;
};
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
window['Blazor'].reconnect = reconnect;
const reconnectTask = reconnect();
if (circuit) {
circuits.push(circuit);
}
await reconnectTask;
function reconnectionFailed(results: boolean[]): boolean {
return !results.reduce((current, next) => current && next, true);
}
}
async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
async function initializeConnection(circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as any).name = 'blazorpack';
const connection = new signalR.HubConnectionBuilder()
@ -51,44 +91,42 @@ async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<
.build();
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
connection.on('JS.RenderBatch', (browserRendererId: number, renderId: number, batchData: Uint8Array) => {
try {
renderBatch(browserRendererId, new OutOfProcessRenderBatch(batchData));
connection.send('OnRenderCompleted', renderId, null);
} catch (ex) {
// If there's a rendering exception, notify server *and* throw on client
connection.send('OnRenderCompleted', renderId, ex.toString());
throw ex;
}
connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => {
logger.log(LogLevel.Information, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`);
const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger);
queue.processBatch(batchId, batchData, connection);
});
connection.onclose(error => circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
connection.on('JS.Error', error => unhandledError(connection, error));
connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
connection.on('JS.Error', error => unhandledError(connection, error, logger));
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
try {
await connection.start();
} catch (ex) {
unhandledError(connection, ex);
unhandledError(connection, ex, logger);
}
DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
}
},
});
return connection;
}
function unhandledError(connection: signalR.HubConnection, err: Error) {
console.error(err);
function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
logger.log(LogLevel.Error, err);
// Disconnect on errors.
//
// Trying to call methods on the connection after its been closed will throw.
if (connection) {
renderingFailed = true;
connection.stop();
}
}

View File

@ -2,13 +2,16 @@ import { CircuitHandler } from './CircuitHandler';
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
import { ReconnectDisplay } from './ReconnectDisplay';
import { ILogger, LogLevel } from '../Logging/ILogger';
export class AutoReconnectCircuitHandler implements CircuitHandler {
static readonly MaxRetries = 5;
static readonly RetryInterval = 3000;
static readonly DialogId = 'components-reconnect-modal';
reconnectDisplay: ReconnectDisplay;
public static readonly MaxRetries = 5;
public static readonly RetryInterval = 3000;
public static readonly DialogId = 'components-reconnect-modal';
public reconnectDisplay: ReconnectDisplay;
public logger: ILogger;
constructor() {
public constructor(logger: ILogger) {
this.logger = logger;
this.reconnectDisplay = new DefaultReconnectDisplay(document);
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
@ -17,15 +20,15 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
}
});
}
onConnectionUp() : void{
public onConnectionUp(): void {
this.reconnectDisplay.hide();
}
delay() : Promise<void>{
public delay(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
}
async onConnectionDown() : Promise<void> {
public async onConnectionDown(): Promise<void> {
this.reconnectDisplay.show();
for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
@ -38,7 +41,7 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
}
return;
} catch (err) {
console.error(err);
this.logger.log(LogLevel.Error, err);
}
}

View File

@ -1,10 +1,10 @@
export interface CircuitHandler {
/** Invoked when a server connection is established or re-established after a connection failure.
/** Invoked when a server connection is established or re-established after a connection failure.
*/
onConnectionUp?() : void;
onConnectionUp?(): void;
/** Invoked when a server connection is dropped.
/** Invoked when a server connection is dropped.
* @param {Error} error Optionally argument containing the error that caused the connection to close (if any).
*/
onConnectionDown?(error?: Error): void;
onConnectionDown?(error?: Error): void;
}

View File

@ -0,0 +1,129 @@
import { internalFunctions as uriHelperFunctions } from '../../Services/UriHelper';
import { ComponentDescriptor, MarkupRegistrationTags, StartComponentComment, EndComponentComment } from './ComponentDescriptor';
export class CircuitDescriptor {
public circuitId: string;
public components: ComponentDescriptor[];
public constructor(circuitId: string, components: ComponentDescriptor[]) {
this.circuitId = circuitId;
this.components = components;
}
public reconnect(reconnection: signalR.HubConnection): Promise<boolean> {
return reconnection.invoke<boolean>('ConnectCircuit', this.circuitId);
}
}
export function discoverPrerenderedCircuits(document: Document): CircuitDescriptor[] {
const commentPairs = resolveCommentPairs(document);
const discoveredCircuits = new Map<string, ComponentDescriptor[]>();
for (let i = 0; i < commentPairs.length; i++) {
const pair = commentPairs[i];
let circuit = discoveredCircuits.get(pair.start.circuitId);
if (!circuit) {
circuit = [];
discoveredCircuits.set(pair.start.circuitId, circuit);
}
const entry = new ComponentDescriptor(pair.start.componentId, pair.start.circuitId, pair.start.rendererId, pair);
circuit.push(entry);
}
const circuits: CircuitDescriptor[] = [];
for (const [key, values] of discoveredCircuits) {
circuits.push(new CircuitDescriptor(key, values));
}
return circuits;
}
export async function startCircuit(connection: signalR.HubConnection): Promise<CircuitDescriptor | undefined> {
const result = await connection.invoke<string>('StartCircuit', uriHelperFunctions.getLocationHref(), uriHelperFunctions.getBaseURI());
if (result) {
return new CircuitDescriptor(result, []);
} else {
return undefined;
}
}
function resolveCommentPairs(node: Node): MarkupRegistrationTags[] {
if (!node.hasChildNodes()) {
return [];
}
const result: MarkupRegistrationTags[] = [];
const children = node.childNodes;
let i = 0;
const childrenLength = children.length;
while (i < childrenLength) {
const currentChildNode = children[i];
const startComponent = getComponentStartComment(currentChildNode);
if (!startComponent) {
i++;
const childResults = resolveCommentPairs(currentChildNode);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
continue;
}
const endComponent = getComponentEndComment(startComponent, children, i + 1, childrenLength);
result.push({ start: startComponent, end: endComponent });
i = endComponent.index + 1;
}
return result;
}
function getComponentStartComment(node: Node): StartComponentComment | undefined {
if (node.nodeType !== Node.COMMENT_NODE) {
return;
}
if (node.textContent) {
const componentStartComment = /\W+M.A.C.Component:[^{]*(.*)$/;
const definition = componentStartComment.exec(node.textContent);
const json = definition && definition[1];
if (json) {
try {
const { componentId, circuitId, rendererId } = JSON.parse(json);
const allComponents = !!componentId && !!circuitId && !!rendererId;
if (allComponents) {
return {
node: node as Comment,
circuitId,
rendererId: Number.parseInt(rendererId),
componentId: Number.parseInt(componentId),
};
} else {
throw new Error(`Found malformed start component comment at ${node.textContent}`);
}
} catch (error) {
throw new Error(`Found malformed start component comment at ${node.textContent}`);
}
}
}
}
function getComponentEndComment(component: StartComponentComment, children: NodeList, index: number, end: number): EndComponentComment {
for (let i = index; i < end; i++) {
const node = children[i];
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}
const componentEndComment = /\W+M.A.C.Component:\W+(\d+)\W+$/;
const definition = componentEndComment.exec(node.textContent);
const rawComponentId = definition && definition[1];
if (!rawComponentId) {
continue;
}
try {
const componentId = Number.parseInt(rawComponentId);
if (componentId === component.componentId) {
return { componentId, node: node as Comment, index: i };
} else {
throw new Error(`Found malformed end component comment at ${node.textContent}`);
}
} catch (error) {
throw new Error(`Found malformed end component comment at ${node.textContent}`);
}
}
throw new Error(`End component comment not found for ${component.node}`);
}

View File

@ -0,0 +1,43 @@
import { attachRootComponentToLogicalElement } from '../../Rendering/Renderer';
import { toLogicalRootCommentElement } from '../../Rendering/LogicalElements';
export interface EndComponentComment {
componentId: number;
node: Comment;
index: number;
}
export interface StartComponentComment {
node: Comment;
rendererId: number;
componentId: number;
circuitId: string;
}
// Represent pairs of start end comments indicating a component that was registered
// in markup (such as a prerendered component)
export interface MarkupRegistrationTags {
start: StartComponentComment;
end: EndComponentComment;
}
export class ComponentDescriptor {
public registrationTags: MarkupRegistrationTags;
public componentId: number;
public circuitId: string;
public rendererId: number;
public constructor(componentId: number, circuitId: string, rendererId: number, descriptor: MarkupRegistrationTags) {
this.componentId = componentId;
this.circuitId = circuitId;
this.rendererId = rendererId;
this.registrationTags = descriptor;
}
public initialize(): void {
const startEndPair = { start: this.registrationTags.start.node, end: this.registrationTags.end.node };
const logicalElement = toLogicalRootCommentElement(startEndPair.start, startEndPair.end);
attachRootComponentToLogicalElement(this.rendererId, logicalElement, this.componentId);
}
}

View File

@ -1,5 +1,5 @@
import { ReconnectDisplay } from "./ReconnectDisplay";
import { AutoReconnectCircuitHandler } from "./AutoReconnectCircuitHandler";
import { ReconnectDisplay } from './ReconnectDisplay';
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
export class DefaultReconnectDisplay implements ReconnectDisplay {
modal: HTMLDivElement;
message: HTMLHeadingElement;
@ -10,18 +10,18 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
this.modal.id = AutoReconnectCircuitHandler.DialogId;
const modalStyles = [
"position: fixed",
"top: 0",
"right: 0",
"bottom: 0",
"left: 0",
"z-index: 1000",
"display: none",
"overflow: hidden",
"background-color: #fff",
"opacity: 0.8",
"text-align: center",
"font-weight: bold"
'position: fixed',
'top: 0',
'right: 0',
'bottom: 0',
'left: 0',
'z-index: 1000',
'display: none',
'overflow: hidden',
'background-color: #fff',
'opacity: 0.8',
'text-align: center',
'font-weight: bold'
];
this.modal.style.cssText = modalStyles.join(';');

View File

@ -0,0 +1,65 @@
import { renderBatch } from '../../Rendering/Renderer';
import { OutOfProcessRenderBatch } from '../../Rendering/RenderBatch/OutOfProcessRenderBatch';
import { ILogger, LogLevel } from '../Logging/ILogger';
import { HubConnection } from '@aspnet/signalr';
export default class RenderQueue {
private static renderQueues = new Map<number, RenderQueue>();
private nextBatchId = 2;
public browserRendererId: number;
public logger: ILogger;
public constructor(browserRendererId: number, logger: ILogger) {
this.browserRendererId = browserRendererId;
this.logger = logger;
}
public static getOrCreateQueue(browserRendererId: number, logger: ILogger): RenderQueue {
const queue = this.renderQueues.get(browserRendererId);
if (queue) {
return queue;
}
const newQueue = new RenderQueue(browserRendererId, logger);
this.renderQueues.set(browserRendererId, newQueue);
return newQueue;
}
public processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): void {
if (receivedBatchId < this.nextBatchId) {
this.logger.log(LogLevel.Information, `Batch ${receivedBatchId} already processed. Waiting for batch ${this.nextBatchId}.`);
return;
}
if (receivedBatchId > this.nextBatchId) {
this.logger.log(LogLevel.Information, `Waiting for batch ${this.nextBatchId}. Batch ${receivedBatchId} not processed.`);
return;
}
try {
this.nextBatchId++;
this.logger.log(LogLevel.Information, `Applying batch ${receivedBatchId}.`);
renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
this.completeBatch(connection, receivedBatchId);
} catch (error) {
this.logger.log(LogLevel.Error, `There was an error applying batch ${receivedBatchId}.`);
// If there's a rendering exception, notify server *and* throw on client
connection.send('OnRenderCompleted', receivedBatchId, error.toString());
throw error;
}
}
public getLastBatchid(): number {
return this.nextBatchId - 1;
}
private async completeBatch(connection: signalR.HubConnection, batchId: number): Promise<void> {
try {
await connection.send('OnRenderCompleted', batchId, null);
} catch {
this.logger.log(LogLevel.Warning, `Failed to deliver completion notification for render '${batchId}'.`);
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// These values are designed to match the ASP.NET Log Levels since that's the pattern we're emulating here.
/** Indicates the severity of a log message.
*
* Log Levels are ordered in increasing severity. So `Debug` is more severe than `Trace`, etc.
*/
export enum LogLevel {
/** Log level for very low severity diagnostic messages. */
Trace = 0,
/** Log level for low severity diagnostic messages. */
Debug = 1,
/** Log level for informational diagnostic messages. */
Information = 2,
/** Log level for diagnostic messages that indicate a non-fatal problem. */
Warning = 3,
/** Log level for diagnostic messages that indicate a failure in the current operation. */
Error = 4,
/** Log level for diagnostic messages that indicate a failure that will terminate the entire application. */
Critical = 5,
/** The highest possible log level. Used when configuring logging to indicate that no log messages should be emitted. */
None = 6,
}
/** An abstraction that provides a sink for diagnostic messages. */
export interface ILogger { // eslint-disable-line @typescript-eslint/interface-name-prefix
/** Called by the framework to emit a diagnostic message.
*
* @param {LogLevel} logLevel The severity level of the message.
* @param {string} message The message.
*/
log(logLevel: LogLevel, message: string | Error): void;
}

View File

@ -0,0 +1,41 @@
/* eslint-disable no-console */
import { ILogger, LogLevel } from './ILogger';
export class NullLogger implements ILogger {
public static instance: ILogger = new NullLogger();
private constructor() { }
public log(_logLevel: LogLevel, _message: string): void { // eslint-disable-line @typescript-eslint/no-unused-vars
}
}
export class ConsoleLogger implements ILogger {
private readonly minimumLogLevel: LogLevel;
public constructor(minimumLogLevel: LogLevel) {
this.minimumLogLevel = minimumLogLevel;
}
public log(logLevel: LogLevel, message: string | Error): void {
if (logLevel >= this.minimumLogLevel) {
switch (logLevel) {
case LogLevel.Critical:
case LogLevel.Error:
console.error(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
case LogLevel.Warning:
console.warn(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
case LogLevel.Information:
console.info(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
default:
// console.debug only goes to attached debuggers in Node, so we use console.log for Trace and Debug
console.log(`[${new Date().toISOString()}] ${LogLevel[logLevel]}: ${message}`);
break;
}
}
}
}

View File

@ -1,31 +1,32 @@
import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
import { EventDelegator } from './EventDelegator';
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
const selectValuePropname = '_blazorSelectValue';
const sharedTemplateElemForParsing = document.createElement('template');
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
const rootComponentsPendingFirstRender: { [componentId: number]: Element } = {};
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
export class BrowserRenderer {
private eventDelegator: EventDelegator;
private childComponentLocations: { [componentId: number]: LogicalElement } = {};
private browserRendererId: number;
constructor(private browserRendererId: number) {
public constructor(browserRendererId: number) {
this.browserRendererId = browserRendererId;
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs) => {
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs);
});
}
public attachRootComponentToElement(componentId: number, element: Element) {
// 'allowExistingContents' to keep any prerendered content until we do the first client-side render
this.attachComponentToElement(componentId, toLogicalElement(element, /* allowExistingContents */ true));
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
this.attachComponentToElement(componentId, element);
rootComponentsPendingFirstRender[componentId] = element;
}
public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>): void {
const element = this.childComponentLocations[componentId];
if (!element) {
throw new Error(`No element is currently associated with component ${componentId}`);
@ -34,8 +35,14 @@ export class BrowserRenderer {
// On the first render for each root component, clear any existing content (e.g., prerendered)
const rootElementToClear = rootComponentsPendingFirstRender[componentId];
if (rootElementToClear) {
const rootElementToClearEnd = getLogicalSiblingEnd(rootElementToClear);
delete rootComponentsPendingFirstRender[componentId];
clearElement(rootElementToClear);
if (!rootElementToClearEnd) {
clearElement(rootElementToClear as unknown as Element);
} else {
clearBetween(rootElementToClear as unknown as Node, rootElementToClearEnd as unknown as Comment);
}
}
this.applyEdits(batch, element, 0, edits, referenceFrames);
@ -89,7 +96,7 @@ export class BrowserRenderer {
if (element instanceof Element) {
this.applyAttribute(batch, element, frame);
} else {
throw new Error(`Cannot set attribute on non-element child`);
throw new Error('Cannot set attribute on non-element child');
}
break;
}
@ -106,7 +113,7 @@ export class BrowserRenderer {
element.removeAttribute(attributeName);
}
} else {
throw new Error(`Cannot remove attribute from non-element child`);
throw new Error('Cannot remove attribute from non-element child');
}
break;
}
@ -118,7 +125,7 @@ export class BrowserRenderer {
if (textNode instanceof Text) {
textNode.textContent = frameReader.textContent(frame);
} else {
throw new Error(`Cannot set text content on non-text child`);
throw new Error('Cannot set text content on non-text child');
}
break;
}
@ -336,6 +343,11 @@ export class BrowserRenderer {
}
}
export interface ComponentDescriptor {
start: Node;
end: Node;
}
function parseMarkup(markup: string, isSvg: boolean) {
if (isSvg) {
sharedSvgElemForParsing.innerHTML = markup || ' ';
@ -369,7 +381,7 @@ function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: num
const eventDescriptor = {
browserRendererId,
eventHandlerId,
eventArgsType: eventArgs.type
eventArgsType: eventArgs.type,
};
return DotNet.invokeMethodAsync(
@ -385,3 +397,22 @@ function clearElement(element: Element) {
element.removeChild(childNode);
}
}
function clearBetween(start: Node, end: Node): void {
const logicalParent = getLogicalParent(start as unknown as LogicalElement);
if(!logicalParent){
throw new Error("Can't clear between nodes. The start node does not have a logical parent.");
}
const children = getLogicalChildrenArray(logicalParent);
const removeStart = children.indexOf(start as unknown as LogicalElement) + 1;
const endIndex = children.indexOf(end as unknown as LogicalElement);
// We remove the end component comment from the DOM as we don't need it after this point.
for (let i = removeStart; i <= endIndex; i++) {
removeLogicalChild(logicalParent, removeStart);
}
// We sanitize the start comment by removing all the information from it now that we don't need it anymore
// as it adds noise to the DOM.
start.textContent = '!';
}

View File

@ -1,4 +1,4 @@
/*
/*
A LogicalElement plays the same role as an Element instance from the point of view of the
API consumer. Inserting and removing logical elements updates the browser DOM just the same.
@ -27,8 +27,40 @@
const logicalChildrenPropname = createSymbolOrFallback('_blazorLogicalChildren');
const logicalParentPropname = createSymbolOrFallback('_blazorLogicalParent');
const logicalEndSiblingPropname = createSymbolOrFallback('_blazorLogicalEnd');
export function toLogicalElement(element: Element, allowExistingContents?: boolean) {
export function toLogicalRootCommentElement(start: Comment, end: Comment): LogicalElement {
// Now that we support start/end comments as component delimiters we are going to be setting up
// adding the components rendered output as siblings of the start/end tags (between).
// For that to work, we need to appropriately configure the parent element to be a logical element
// with all their children being the child elements.
// For example, imagine you have
// <app>
// <div><p>Static content</p></div>
// <!-- start component
// <!-- end component
// <footer>Some other content</footer>
// <app>
// We want the parent element to be something like
// *app
// |- *div
// |- *component
// |- *footer
if(!start.parentNode){
throw new Error(`Comment not connected to the DOM ${start.textContent}`);
}
const parent = start.parentNode;
const parentLogicalElement = toLogicalElement(parent, /* allow existing contents */ true);
const children = getLogicalChildrenArray(parentLogicalElement);
Array.from(parent.childNodes).forEach(n => children.push(n as unknown as LogicalElement));
start[logicalParentPropname] = parentLogicalElement;
start[logicalEndSiblingPropname] = end;
toLogicalElement(end, /* allowExistingcontents */ true);
return toLogicalElement(start, /* allowExistingContents */ true);
}
export function toLogicalElement(element: Node, allowExistingContents?: boolean): LogicalElement {
// Normally it's good to assert that the element has started empty, because that's the usual
// situation and we probably have a bug if it's not. But for the element that contain prerendered
// root components, we want to let them keep their content until we replace it.
@ -37,7 +69,7 @@ export function toLogicalElement(element: Element, allowExistingContents?: boole
}
element[logicalChildrenPropname] = [];
return element as any as LogicalElement;
return element as unknown as LogicalElement;
}
export function createAndInsertLogicalContainer(parent: LogicalElement, childIndex: number): LogicalElement {
@ -107,6 +139,10 @@ export function getLogicalParent(element: LogicalElement): LogicalElement | null
return (element[logicalParentPropname] as LogicalElement) || null;
}
export function getLogicalSiblingEnd(element: LogicalElement): LogicalElement | null {
return (element[logicalEndSiblingPropname] as LogicalElement) || null;
}
export function getLogicalChild(parent: LogicalElement, childIndex: number): LogicalElement {
return getLogicalChildrenArray(parent)[childIndex];
}
@ -115,7 +151,7 @@ export function isSvgElement(element: LogicalElement) {
return getClosestDomElement(element).namespaceURI === 'http://www.w3.org/2000/svg';
}
function getLogicalChildrenArray(element: LogicalElement) {
export function getLogicalChildrenArray(element: LogicalElement) {
return element[logicalChildrenPropname] as LogicalElement[];
}
@ -161,4 +197,4 @@ function createSymbolOrFallback(fallback: string): symbol | string {
}
// Nominal type to represent a logical element without needing to allocate any object for instances
export interface LogicalElement { LogicalElement__DO_NOT_IMPLEMENT: any };
export interface LogicalElement { LogicalElement__DO_NOT_IMPLEMENT: any }

View File

@ -1,25 +1,37 @@
/* eslint-disable @typescript-eslint/camelcase */
import { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform';
import { platform } from '../Environment';
import { RenderBatch } from './RenderBatch/RenderBatch';
import { BrowserRenderer } from './BrowserRenderer';
import { toLogicalElement, LogicalElement } from './LogicalElements';
type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer };
interface BrowserRendererRegistry {
[browserRendererId: number]: BrowserRenderer;
}
const browserRenderers: BrowserRendererRegistry = {};
export function attachRootComponentToElement(browserRendererId: number, elementSelector: string, componentId: number) {
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Could not find any element matching selector '${elementSelector}'.`);
}
export function attachRootComponentToLogicalElement(browserRendererId: number, logicalElement: LogicalElement, componentId: number): void {
let browserRenderer = browserRenderers[browserRendererId];
if (!browserRenderer) {
browserRenderer = browserRenderers[browserRendererId] = new BrowserRenderer(browserRendererId);
}
browserRenderer.attachRootComponentToElement(componentId, element);
browserRenderer.attachRootComponentToLogicalElement(componentId, logicalElement);
}
export function renderBatch(browserRendererId: number, batch: RenderBatch) {
export function attachRootComponentToElement(browserRendererId: number, elementSelector: string, componentId: number): void {
const element = document.querySelector(elementSelector);
if (!element) {
throw new Error(`Could not find any element matching selector '${elementSelector}'.`);
}
// 'allowExistingContents' to keep any prerendered content until we do the first client-side render
attachRootComponentToLogicalElement(browserRendererId, toLogicalElement(element, /* allow existing contents */ true), componentId);
}
export function renderBatch(browserRendererId: number, batch: RenderBatch): void {
const browserRenderer = browserRenderers[browserRendererId];
if (!browserRenderer) {
throw new Error(`There is no browser renderer with ID ${browserRendererId}.`);

View File

@ -1,3 +1,3 @@
{
"extends": "../tsconfig.base.json"
"extends": "../tsconfig.json"
}

View File

@ -3,11 +3,12 @@ import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconn
import { UserSpecifiedDisplay } from "../src/Platform/Circuits/UserSpecifiedDisplay";
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
import { ReconnectDisplay } from "../src/Platform/Circuits/ReconnectDisplay";
import { NullLogger} from '../src/Platform/Logging/Loggers';
import '../src/GlobalExports';
describe('AutoReconnectCircuitHandler', () => {
it('creates default element', () => {
const handler = new AutoReconnectCircuitHandler();
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(handler.reconnectDisplay).toBeInstanceOf(DefaultReconnectDisplay);
@ -17,7 +18,7 @@ describe('AutoReconnectCircuitHandler', () => {
const element = document.createElement('div');
element.id = 'components-reconnect-modal';
document.body.appendChild(element);
const handler = new AutoReconnectCircuitHandler();
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
document.dispatchEvent(new Event('DOMContentLoaded'));
expect(handler.reconnectDisplay).toBeInstanceOf(UserSpecifiedDisplay);
@ -32,7 +33,7 @@ describe('AutoReconnectCircuitHandler', () => {
}));
it('hides display on connection up', () => {
const handler = new AutoReconnectCircuitHandler();
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
const testDisplay = new TestDisplay();
handler.reconnectDisplay = testDisplay;
@ -43,7 +44,7 @@ describe('AutoReconnectCircuitHandler', () => {
});
it('shows display on connection down', async () => {
const handler = new AutoReconnectCircuitHandler();
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
handler.delay = () => Promise.resolve();
const reconnect = jest.fn().mockResolvedValue(true);
window['Blazor'].reconnect = reconnect;
@ -59,7 +60,7 @@ describe('AutoReconnectCircuitHandler', () => {
});
it('invokes failed if reconnect fails', async () => {
const handler = new AutoReconnectCircuitHandler();
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
handler.delay = () => Promise.resolve();
const reconnect = jest.fn().mockRejectedValue(new Error('some error'));
window.console.error = jest.fn();

View File

@ -0,0 +1,123 @@
(global as any).DotNet = { attachReviver: jest.fn() };
import { discoverPrerenderedCircuits } from '../src/Platform/Circuits/CircuitManager';
import { NullLogger } from '../src/Platform/Logging/Loggers';
import { JSDOM } from 'jsdom';
describe('CircuitManager', () => {
it('discoverPrerenderedCircuits returns discovered prerendered circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"1"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer></footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
expect(results.length).toEqual(1);
expect(results[0].components.length).toEqual(1);
const result = results[0].components[0];
expect(result.circuitId).toEqual("1234");
expect(result.rendererId).toEqual(2);
expect(result.componentId).toEqual(1);
});
it('discoverPrerenderedCircuits returns discovers multiple prerendered circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"1"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"2"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
</footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
expect(results.length).toEqual(1);
expect(results[0].components.length).toEqual(2);
const first = results[0].components[0];
expect(first.circuitId).toEqual("1234");
expect(first.rendererId).toEqual(2);
expect(first.componentId).toEqual(1);
const second = results[0].components[1];
expect(second.circuitId).toEqual("1234");
expect(second.rendererId).toEqual(2);
expect(second.componentId).toEqual(2);
});
it('discoverPrerenderedCircuits throws for malformed circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"1"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"2"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
</footer>
</body>
</html>`);
expect(() => discoverPrerenderedCircuits(dom.window.document))
.toThrow();
});
it('discoverPrerenderedCircuits initializes circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"1"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"2"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
</footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
for (let i = 0; i < results.length; i++) {
const result = results[i];
for (let j = 0; j < result.components.length; j++) {
const component = result.components[j];
component.initialize();
}
}
});
});

View File

@ -0,0 +1,59 @@
(global as any).DotNet = { attachReviver: jest.fn() };
import RenderQueue from '../src/Platform/Circuits/RenderQueue';
import { NullLogger } from '../src/Platform/Logging/Loggers';
import * as signalR from '@aspnet/signalr';
jest.mock('../src/Rendering/Renderer', () => ({
renderBatch: jest.fn()
}));
describe('RenderQueue', () => {
it('getOrCreateRenderQueue returns a new queue if one does not exist for a renderer', () => {
const queue = RenderQueue.getOrCreateQueue(1, NullLogger.instance);
expect(queue).toBeDefined();
});
it('getOrCreateRenderQueue returns an existing queue if one exists for a renderer', () => {
const queue = RenderQueue.getOrCreateQueue(2, NullLogger.instance);
const secondQueue = RenderQueue.getOrCreateQueue(2, NullLogger.instance);
expect(secondQueue).toBe(queue);
});
it('processBatch does not render previous batches', () => {
const queue = RenderQueue.getOrCreateQueue(3, NullLogger.instance);
const sendMock = jest.fn();
const connection = { send: sendMock } as any as signalR.HubConnection;
queue.processBatch(1, new Uint8Array(0), connection);
expect(sendMock.mock.calls.length).toEqual(0);
});
it('processBatch does not render out of order batches', () => {
const queue = RenderQueue.getOrCreateQueue(4, NullLogger.instance);
const sendMock = jest.fn();
const connection = { send: sendMock } as any as signalR.HubConnection;
queue.processBatch(3, new Uint8Array(0), connection);
expect(sendMock.mock.calls.length).toEqual(0);
});
it('processBatch renders pending batches', () => {
const queue = RenderQueue.getOrCreateQueue(5, NullLogger.instance);
const sendMock = jest.fn();
const connection = { send: sendMock } as any as signalR.HubConnection;
queue.processBatch(2, new Uint8Array(0), connection);
expect(sendMock.mock.calls.length).toEqual(1);
expect(queue.getLastBatchid()).toEqual(2);
});
});

View File

@ -1,3 +1,3 @@
{
"extends": "../tsconfig.base.json"
"extends": "../tsconfig.json",
}

View File

@ -4,6 +4,7 @@
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"downlevelIteration": true,
"target": "es5",
"lib": ["es2015", "dom"],
"strict": true

View File

@ -201,6 +201,33 @@
resolved "https://registry.yarnpkg.com/@types/webassembly-js-api/-/webassembly-js-api-0.0.2.tgz#43a04bd75fa20332133c6c3986156bfeb4a3ced7"
integrity sha512-htlxJRag6RUiMYUkS8Fjup+TMHO0VarpiF9MrqYaGJ0wXtIraQFz40rfA8VIeCiWy8sgpv3RLmigpgicG8fqGA==
"@typescript-eslint/eslint-plugin@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.5.0.tgz#85c509bcfc2eb35f37958fa677379c80b7a8f66f"
integrity sha512-TZ5HRDFz6CswqBUviPX8EfS+iOoGbclYroZKT3GWGYiGScX0qo6QjHc5uuM7JN920voP2zgCkHgF5SDEVlCtjQ==
dependencies:
"@typescript-eslint/parser" "1.5.0"
"@typescript-eslint/typescript-estree" "1.5.0"
requireindex "^1.2.0"
tsutils "^3.7.0"
"@typescript-eslint/parser@1.5.0", "@typescript-eslint/parser@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.5.0.tgz#a96114d195dff2a49534e4c4850fb676f905a072"
integrity sha512-pRWTnJrnxuT0ragdY26hZL+bxqDd4liMlftpH2CBlMPryOIOb1J+MdZuw6R4tIu6bWVdwbHKPTs+Q34LuGvfGw==
dependencies:
"@typescript-eslint/typescript-estree" "1.5.0"
eslint-scope "^4.0.0"
eslint-visitor-keys "^1.0.0"
"@typescript-eslint/typescript-estree@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.5.0.tgz#986b356ecdf5a0c3bc9889d221802149cf5dbd4e"
integrity sha512-XqR14d4BcYgxcrpxIwcee7UEjncl9emKc/MgkeUfIk2u85KlsGYyaxC7Zxjmb17JtWERk/NaO+KnBsqgpIXzwA==
dependencies:
lodash.unescape "4.0.1"
semver "5.5.0"
"@webassemblyjs/ast@1.8.3":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.3.tgz#63a741bd715a6b6783f2ea5c6ab707516aa215eb"
@ -380,6 +407,11 @@ acorn-globals@^4.1.0:
acorn "^6.0.1"
acorn-walk "^6.0.1"
acorn-jsx@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
acorn-walk@^6.0.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
@ -395,6 +427,11 @@ acorn@^6.0.1, acorn@^6.0.5:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==
acorn@^6.0.7:
version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
ajv-errors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
@ -415,7 +452,17 @@ ajv@^6.1.0, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-escapes@^3.0.0:
ajv@^6.9.1:
version "6.10.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
dependencies:
fast-deep-equal "^2.0.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-escapes@^3.0.0, ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
@ -435,6 +482,11 @@ ansi-regex@^4.0.0:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==
ansi-regex@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@ -872,7 +924,7 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@ -881,6 +933,11 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chokidar@^2.0.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058"
@ -935,6 +992,18 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
dependencies:
restore-cursor "^2.0.0"
cli-width@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
@ -1166,7 +1235,7 @@ debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
dependencies:
ms "2.0.0"
debug@^4.1.0, debug@^4.1.1:
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@ -1276,6 +1345,13 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@ -1319,6 +1395,11 @@ elliptic@^6.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@ -1400,6 +1481,75 @@ eslint-scope@^4.0.0:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
dependencies:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-utils@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
eslint-visitor-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
eslint@^5.16.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.9.1"
chalk "^2.1.0"
cross-spawn "^6.0.5"
debug "^4.0.1"
doctrine "^3.0.0"
eslint-scope "^4.0.3"
eslint-utils "^1.3.1"
eslint-visitor-keys "^1.0.0"
espree "^5.0.1"
esquery "^1.0.1"
esutils "^2.0.2"
file-entry-cache "^5.0.1"
functional-red-black-tree "^1.0.1"
glob "^7.1.2"
globals "^11.7.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
inquirer "^6.2.2"
js-yaml "^3.13.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.3.0"
lodash "^4.17.11"
minimatch "^3.0.4"
mkdirp "^0.5.1"
natural-compare "^1.4.0"
optionator "^0.8.2"
path-is-inside "^1.0.2"
progress "^2.0.0"
regexpp "^2.0.1"
semver "^5.5.1"
strip-ansi "^4.0.0"
strip-json-comments "^2.0.1"
table "^5.2.3"
text-table "^0.2.0"
espree@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==
dependencies:
acorn "^6.0.7"
acorn-jsx "^5.0.0"
eslint-visitor-keys "^1.0.0"
esprima@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
@ -1410,6 +1560,13 @@ esprima@^4.0.0:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
dependencies:
estraverse "^4.0.0"
esrecurse@^4.1.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
@ -1417,7 +1574,7 @@ esrecurse@^4.1.0:
dependencies:
estraverse "^4.1.0"
estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
@ -1523,6 +1680,15 @@ extend@~3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
external-editor@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==
dependencies:
chardet "^0.7.0"
iconv-lite "^0.4.24"
tmp "^0.0.33"
extglob@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
@ -1574,6 +1740,20 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
dependencies:
flat-cache "^2.0.1"
fileset@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
@ -1618,6 +1798,20 @@ findup-sync@^2.0.0:
micromatch "^3.0.4"
resolve-dir "^1.0.1"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
dependencies:
flatted "^2.0.0"
rimraf "2.6.3"
write "1.0.3"
flatted@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@ -1695,6 +1889,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@ -1773,7 +1972,7 @@ global-prefix@^1.0.1:
is-windows "^1.0.1"
which "^1.2.14"
globals@^11.1.0:
globals@^11.1.0, globals@^11.7.0:
version "11.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==
@ -1923,7 +2122,7 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
iconv-lite@0.4.24, iconv-lite@^0.4.4:
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -1947,6 +2146,19 @@ ignore-walk@^3.0.1:
dependencies:
minimatch "^3.0.4"
ignore@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
import-fresh@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
import-local@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
@ -1988,6 +2200,25 @@ ini@^1.3.4, ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
inquirer@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406"
integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==
dependencies:
ansi-escapes "^3.2.0"
chalk "^2.4.2"
cli-cursor "^2.1.0"
cli-width "^2.0.0"
external-editor "^3.0.3"
figures "^2.0.0"
lodash "^4.17.11"
mute-stream "0.0.7"
run-async "^2.2.0"
rxjs "^6.4.0"
string-width "^2.1.0"
strip-ansi "^5.0.0"
through "^2.3.6"
interpret@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
@ -2147,6 +2378,11 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
is-regex@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
@ -2621,6 +2857,14 @@ js-yaml@^3.12.0:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^3.13.0:
version "3.13.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e"
integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -2678,6 +2922,11 @@ json-schema@0.2.3:
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@ -2753,7 +3002,7 @@ leven@^2.1.0:
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
levn@~0.3.0:
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
@ -2798,6 +3047,11 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash.unescape@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
lodash@^4.17.10, lodash@^4.17.11:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
@ -3052,6 +3306,11 @@ msgpack5@^4.0.2:
readable-stream "^2.3.6"
safe-buffer "^5.1.2"
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
nan@^2.9.2:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
@ -3287,6 +3546,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
dependencies:
mimic-fn "^1.0.0"
optimist@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@ -3295,7 +3561,7 @@ optimist@^0.6.1:
minimist "~0.0.1"
wordwrap "~0.0.2"
optionator@^0.8.1:
optionator@^0.8.1, optionator@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
@ -3333,7 +3599,7 @@ os-locale@^3.0.0:
lcid "^2.0.0"
mem "^4.0.0"
os-tmpdir@^1.0.0:
os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@ -3406,6 +3672,13 @@ parallel-transform@^1.1.0:
inherits "^2.0.3"
readable-stream "^2.1.5"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
parse-asn1@^5.0.0:
version "5.1.4"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc"
@ -3461,6 +3734,11 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-is-inside@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
@ -3546,6 +3824,11 @@ process@^0.11.10:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@ -3720,6 +4003,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexpp@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@ -3787,6 +4075,11 @@ require-main-filename@^1.0.1:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@ -3812,6 +4105,11 @@ resolve-from@^3.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
integrity sha1-six699nWiBvItuZTM17rywoYh0g=
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -3829,12 +4127,20 @@ resolve@1.x, resolve@^1.10.0, resolve@^1.3.2:
dependencies:
path-parse "^1.0.6"
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
dependencies:
onetime "^2.0.0"
signal-exit "^3.0.2"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
@ -3854,6 +4160,13 @@ rsvp@^3.3.3:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==
run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
dependencies:
is-promise "^2.1.0"
run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
@ -3861,6 +4174,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rxjs@^6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==
dependencies:
tslib "^1.9.0"
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@ -3914,6 +4234,16 @@ schema-utils@^1.0.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
semver@5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
semver@^5.5.1:
version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
serialize-javascript@^1.4.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879"
@ -3989,6 +4319,15 @@ slash@^2.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
dependencies:
ansi-styles "^3.2.0"
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@ -4185,7 +4524,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@ -4193,6 +4532,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string-width@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
dependencies:
emoji-regex "^7.0.1"
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
string_decoder@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@ -4228,6 +4576,13 @@ strip-ansi@^5.0.0:
dependencies:
ansi-regex "^4.0.0"
strip-ansi@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
dependencies:
ansi-regex "^4.1.0"
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@ -4238,7 +4593,7 @@ strip-eof@^1.0.0:
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
strip-json-comments@~2.0.1:
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
@ -4262,6 +4617,16 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
table@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2"
integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==
dependencies:
ajv "^6.9.1"
lodash "^4.17.11"
slice-ansi "^2.1.0"
string-width "^3.0.0"
tapable@^1.0.0, tapable@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
@ -4313,6 +4678,11 @@ test-exclude@^5.0.0:
read-pkg-up "^4.0.0"
require-main-filename "^1.0.1"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
throat@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
@ -4326,6 +4696,11 @@ through2@^2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
timers-browserify@^2.0.4:
version "2.0.10"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
@ -4333,6 +4708,13 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
dependencies:
os-tmpdir "~1.0.2"
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
@ -4427,11 +4809,18 @@ ts-loader@^4.4.1:
micromatch "^3.1.4"
semver "^5.0.1"
tslib@^1.9.0:
tslib@^1.8.1, tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tsutils@^3.7.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.9.1.tgz#2a40dc742943c71eca6d5c1994fcf999956be387"
integrity sha512-hrxVtLtPqQr//p8/msPT1X1UYXUjizqSit5d9AQ5k38TcV38NyecL5xODNxa73cLe/5sdiJ+w1FqzDhRBA/anA==
dependencies:
tslib "^1.8.1"
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -4461,10 +4850,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^2.9.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
typescript@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6"
integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==
uglify-js@^3.1.4:
version "3.4.9"
@ -4784,6 +5173,13 @@ write-file-atomic@2.4.1:
imurmurhash "^0.1.4"
signal-exit "^3.0.2"
write@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
dependencies:
mkdirp "^0.5.1"
ws@^5.2.0:
version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"

View File

@ -638,13 +638,21 @@ namespace Microsoft.AspNetCore.Components.Layouts
}
namespace Microsoft.AspNetCore.Components.Rendering
{
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct ComponentRenderedText
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.Collections.Generic.IEnumerable<string> Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
{
public HtmlRenderer(System.IServiceProvider serviceProvider, System.Func<string, string> htmlEncoder, Microsoft.AspNetCore.Components.Rendering.IDispatcher dispatcher) : base (default(System.IServiceProvider)) { }
protected override void HandleException(System.Exception exception) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
public System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> RenderComponentAsync<TComponent>(Microsoft.AspNetCore.Components.ParameterCollection initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync<TComponent>(Microsoft.AspNetCore.Components.ParameterCollection initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch renderBatch) { throw null; }
}
public partial interface IDispatcher
@ -792,9 +800,10 @@ namespace Microsoft.AspNetCore.Components.Services
{
protected UriHelperBase() { }
public event System.EventHandler<string> OnLocationChanged { add { } remove { } }
protected virtual void EnsureInitialized() { }
public string GetAbsoluteUri() { throw null; }
public virtual string GetBaseUri() { throw null; }
protected virtual void InitializeState() { }
public virtual void InitializeState(string uriAbsolute, string baseUriAbsolute) { }
public void NavigateTo(string uri) { }
public void NavigateTo(string uri, bool forceLoad) { }
protected abstract void NavigateToCore(string uri, bool forceLoad);

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Components.Rendering
{
/// <summary>
/// Represents the result of rendering a component into static html.
/// </summary>
public readonly struct ComponentRenderedText
{
internal ComponentRenderedText(int componentId, IEnumerable<string> tokens)
{
ComponentId = componentId;
Tokens = tokens;
}
/// <summary>
/// Gets the id associated with the component.
/// </summary>
public int ComponentId { get; }
/// <summary>
/// Gets the sequence of tokens that when concatenated represent the html for the rendered component.
/// </summary>
public IEnumerable<string> Tokens { get; }
}
}

View File

@ -47,21 +47,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
{
var frames = await CreateInitialRenderAsync(componentType, initialParameters);
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
if (frames.Count == 0)
{
return Array.Empty<string>();
}
else
{
var result = new List<string>();
var newPosition = RenderFrames(result, frames, 0, frames.Count);
Debug.Assert(newPosition == frames.Count);
return result;
}
var result = new List<string>();
var newPosition = RenderFrames(result, frames, 0, frames.Count);
Debug.Assert(newPosition == frames.Count);
return new ComponentRenderedText(componentId, result);
}
/// <summary>
@ -71,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <typeparam name="TComponent">The type of the <see cref="IComponent"/>.</typeparam>
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
public Task<IEnumerable<string>> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
{
return RenderComponentAsync(typeof(TComponent), initialParameters);
}
@ -227,14 +220,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
return position + maxElements;
}
private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
private async Task<(int, ArrayRange<RenderTreeFrame>)> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
await RenderRootComponentAsync(componentId, initialParameters);
return GetCurrentRenderTreeFrames(componentId);
return (componentId, GetCurrentRenderTreeFrames(componentId));
}
}
}

View File

@ -227,6 +227,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
task = callback.InvokeAsync(eventArgs);
}
catch (Exception e)
{
HandleException(e);
}
finally
{
_isBatchInProgress = false;
@ -336,7 +340,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
// The pendingTasks collection is only used during prerendering to track quiescence,
// so will be null at other times.
_pendingTasks?.Add(handledErrorTask);
break;
}
}
@ -442,7 +446,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
// Fire off the execution of OnAfterRenderAsync, but don't wait for it
// if there is async work to be done.
_ = InvokeRenderCompletedCalls(batch.UpdatedComponents);
_ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask);
}
catch (Exception e)
{
// Ensure we catch errors while running the render functions of the components.
HandleException(e);
}
finally
{
@ -461,8 +470,34 @@ namespace Microsoft.AspNetCore.Components.Rendering
}
}
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents, Task updateDisplayTask)
{
if (updateDisplayTask.IsCanceled)
{
// The display update was cancelled (maybe due to a timeout on the components server-side case or due
// to the renderer being disposed)
return Task.CompletedTask;
}
if (updateDisplayTask.IsFaulted)
{
// The display update failed so we don't care any more about running on render completed
// fallbacks as the entire rendering process is going to be torn down.
HandleException(updateDisplayTask.Exception);
return Task.CompletedTask;
}
if (!updateDisplayTask.IsCompleted)
{
var updatedComponentsId = new int[updatedComponents.Count];
var updatedComponentsArray = updatedComponents.Array;
for (int i = 0; i < updatedComponentsId.Length; i++)
{
updatedComponentsId[i] = updatedComponentsArray[i].ComponentId;
}
return InvokeRenderCompletedCallsAfterUpdateDisplayTask(updateDisplayTask, updatedComponentsId);
}
List<Task> batch = null;
var array = updatedComponents.Array;
for (var i = 0; i < updatedComponents.Count; i++)
@ -470,36 +505,83 @@ namespace Microsoft.AspNetCore.Components.Rendering
var componentState = GetOptionalComponentState(array[i].ComponentId);
if (componentState != null)
{
// The component might be rendered and disposed in the same batch (if its parent
// was rendered later in the batch, and removed the child from the tree).
var task = componentState.NotifyRenderCompletedAsync();
// We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
// unless we absolutely have to.
if (task.IsCompleted)
{
if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
{
// Nothing to do here.
continue;
}
else if (task.Status == TaskStatus.Faulted)
{
HandleException(task.Exception);
continue;
}
}
// The Task is incomplete.
// Queue up the task and we can inspect it later.
batch = batch ?? new List<Task>();
batch.Add(GetErrorHandledTask(task));
NotifyRenderCompleted(componentState, ref batch);
}
}
return batch != null ?
Task.WhenAll(batch) :
Task.CompletedTask;
}
private async Task InvokeRenderCompletedCallsAfterUpdateDisplayTask(
Task updateDisplayTask,
int[] updatedComponents)
{
try
{
await updateDisplayTask;
}
catch when (updateDisplayTask.IsCanceled)
{
return;
}
catch when (updateDisplayTask.IsFaulted)
{
HandleException(updateDisplayTask.Exception);
return;
}
List<Task> batch = null;
var array = updatedComponents;
for (var i = 0; i < updatedComponents.Length; i++)
{
var componentState = GetOptionalComponentState(array[i]);
if (componentState != null)
{
NotifyRenderCompleted(componentState, ref batch);
}
}
var result = batch != null ?
Task.WhenAll(batch) :
Task.CompletedTask;
await result;
}
private void NotifyRenderCompleted(ComponentState state, ref List<Task> batch)
{
// The component might be rendered and disposed in the same batch (if its parent
// was rendered later in the batch, and removed the child from the tree).
// This can also happen between batches if the UI takes some time to update and within
// that time the component gets removed out of the tree because the parent chose not to
// render it in a later batch.
// In any of the two cases mentioned happens, OnAfterRenderAsync won't run but that is
// ok.
var task = state.NotifyRenderCompletedAsync();
// We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
// unless we absolutely have to.
if (task.IsCompleted)
{
if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
{
// Nothing to do here.
return;
}
else if (task.Status == TaskStatus.Faulted)
{
HandleException(task.Exception);
return;
}
}
// The Task is incomplete.
// Queue up the task and we can inspect it later.
batch = batch ?? new List<Task>();
batch.Add(GetErrorHandledTask(task));
}
private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Services
{
add
{
EnsureInitialized();
AssertInitialized();
_onLocationChanged += value;
}
remove
@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Components.Services
/// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
public void NavigateTo(string uri, bool forceLoad)
{
EnsureInitialized();
AssertInitialized();
NavigateToCore(uri, forceLoad);
}
@ -70,10 +70,35 @@ namespace Microsoft.AspNetCore.Components.Services
protected abstract void NavigateToCore(string uri, bool forceLoad);
/// <summary>
/// Called to initialize BaseURI and current URI before those values the first time.
/// Override this method to dynamically calculate the those values.
/// Called to initialize BaseURI and current URI before these values are used for the first time.
/// Override this method to dynamically calculate these values.
/// </summary>
protected virtual void InitializeState()
public virtual void InitializeState(string uriAbsolute, string baseUriAbsolute)
{
if (uriAbsolute == null)
{
throw new ArgumentNullException(nameof(uriAbsolute));
}
if (baseUriAbsolute == null)
{
throw new ArgumentNullException(nameof(baseUriAbsolute));
}
if (_isInitialized)
{
throw new InvalidOperationException($"'{typeof(UriHelperBase).Name}' already initialized.");
}
_isInitialized = true;
SetAbsoluteUri(uriAbsolute);
SetAbsoluteBaseUri(baseUriAbsolute);
}
/// <summary>
/// Allows derived classes to lazyly self initialize. It does nothing unless overriden.
/// </summary>
protected virtual void EnsureInitialized()
{
}
@ -83,7 +108,7 @@ namespace Microsoft.AspNetCore.Components.Services
/// <returns>The current absolute URI.</returns>
public string GetAbsoluteUri()
{
EnsureInitialized();
AssertInitialized();
return _uri;
}
@ -95,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Services
/// <returns>The URI prefix, which has a trailing slash.</returns>
public virtual string GetBaseUri()
{
EnsureInitialized();
AssertInitialized();
return _baseUriString;
}
@ -107,7 +132,7 @@ namespace Microsoft.AspNetCore.Components.Services
/// <returns>The absolute URI.</returns>
public Uri ToAbsoluteUri(string href)
{
EnsureInitialized();
AssertInitialized();
return new Uri(_baseUri, href);
}
@ -185,12 +210,16 @@ namespace Microsoft.AspNetCore.Components.Services
_onLocationChanged?.Invoke(this, _uri);
}
private void EnsureInitialized()
private void AssertInitialized()
{
if (!_isInitialized)
{
InitializeState();
_isInitialized = true;
EnsureInitialized();
}
if (!_isInitialized)
{
throw new InvalidOperationException($"'{GetType().Name}' has not been initialized.");
}
}
}

View File

@ -2454,6 +2454,50 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal(2, component.OnAfterRenderCallCount);
}
[Fact]
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously()
{
// Arrange
var tcs = new TaskCompletionSource<object>();
var afterRenderTcs = new TaskCompletionSource<object>();
var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task);
var renderer = new AsyncUpdateTestRenderer()
{
OnUpdateDisplayAsync = _ => tcs.Task
};
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
tcs.SetResult(null);
afterRenderTcs.SetResult(null);
// Assert
Assert.True(component.Called);
}
[Fact]
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingSynchronously()
{
// Arrange
var afterRenderTcs = new TaskCompletionSource<object>();
var onAfterRenderCallCountLog = new List<int>();
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task);
var renderer = new AsyncUpdateTestRenderer()
{
OnUpdateDisplayAsync = _ => Task.CompletedTask
};
renderer.AssignRootComponentId(component);
// Act
component.TriggerRender();
afterRenderTcs.SetResult(null);
// Assert
Assert.True(component.Called);
}
[Fact]
public void DoesNotCallOnAfterRenderForComponentsNotRendered()
{
@ -3763,5 +3807,39 @@ namespace Microsoft.AspNetCore.Components.Test
await TaskToAwait;
}
}
private class AsyncUpdateTestRenderer : TestRenderer
{
public Func<RenderBatch, Task> OnUpdateDisplayAsync { get; set; }
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
{
return OnUpdateDisplayAsync(renderBatch);
}
}
private class AsyncAfterRenderComponent : AutoRenderComponent, IHandleAfterRender
{
private readonly Task _task;
public AsyncAfterRenderComponent(Task task)
{
_task = task;
}
public bool Called { get; private set; }
public async Task OnAfterRenderAsync()
{
await _task;
Called = true;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.CloseElement();
}
}
}
}

View File

@ -390,12 +390,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
Assert.Equal(expectedHtml, result);
}
private IEnumerable<string> GetResult(Task<IEnumerable<string>> task)
private IEnumerable<string> GetResult(Task<ComponentRenderedText> task)
{
Assert.True(task.IsCompleted);
if (task.IsCompletedSuccessfully)
{
return task.Result;
return task.Result.Tokens;
}
else
{
@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
})));
// Assert
Assert.Equal(expectedHtml, result);
Assert.Equal(expectedHtml, result.Tokens);
}
[Fact]
@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
})));
// Assert
Assert.Equal(expectedHtml, result);
Assert.Equal(expectedHtml, result.Tokens);
}

View File

@ -432,6 +432,10 @@ namespace Microsoft.AspNetCore.Components.Server.BlazorPack
writer.Write(byteArray);
break;
case Exception exception:
writer.Write(exception.ToString());
break;
default:
throw new FormatException($"Unsupported argument type {argument.GetType()}");
}

View File

@ -12,6 +12,22 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public static class ComponentEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps the SignalR <see cref="ComponentHub"/> to the path <paramref name="path"/> and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static IEndpointConventionBuilder MapComponentHub(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
return endpoints.MapHub<ComponentHub>(ComponentHub.DefaultPath);
}
/// <summary>
/// Maps the SignalR <see cref="ComponentHub"/> to the path <paramref name="path"/> and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.

View File

@ -1,45 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
/// <summary>
/// Behaves like a <see cref="TaskCompletionSource{T}"/>, but automatically times out
/// the underlying task after a given period if not already completed.
/// </summary>
internal class AutoCancelTaskCompletionSource<T>
{
private readonly TaskCompletionSource<T> _completionSource;
private readonly CancellationTokenSource _timeoutSource;
public AutoCancelTaskCompletionSource(int timeoutMilliseconds)
{
_completionSource = new TaskCompletionSource<T>();
_timeoutSource = new CancellationTokenSource();
_timeoutSource.CancelAfter(timeoutMilliseconds);
_timeoutSource.Token.Register(() => _completionSource.TrySetCanceled());
}
public Task Task => _completionSource.Task;
public void TrySetResult(T result)
{
if (_completionSource.TrySetResult(result))
{
_timeoutSource.Dispose(); // We're not going to time out
}
}
public void TrySetException(Exception exception)
{
if (_completionSource.TrySetException(exception))
{
_timeoutSource.Dispose(); // We're not going to time out
}
}
}
}

View File

@ -10,9 +10,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitClientProxy : IClientProxy
{
public static readonly CircuitClientProxy OfflineClient = new CircuitClientProxy();
private CircuitClientProxy()
public CircuitClientProxy()
{
Connected = false;
}

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
@ -95,15 +96,42 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public IDispatcher Dispatcher { get; }
public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
public Task<ComponentRenderedText> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
{
return Dispatcher.InvokeAsync(async () =>
{
var result = await Renderer.RenderComponentAsync(componentType, parameters);
// When we prerender we start the circuit in a disconnected state. As such, we only call
// OnCircuitOpenenedAsync here and when the client reconnects we run OnConnectionUpAsync
await OnCircuitOpenedAsync(CancellationToken.None);
return result;
});
}
internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException)
{
if (!_initialized)
{
_initialized = true;
UnhandledException += unhandledException;
var uriHelper = (RemoteUriHelper)Services.GetRequiredService<IUriHelper>();
if (!uriHelper.HasAttachedJSRuntime)
{
uriHelper.AttachJsRuntime(JSRuntime);
}
}
}
internal void SendPendingBatches()
{
// Dispatch any buffered renders we accumulated during a disconnect.
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
var _ = Renderer.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
}
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await Renderer.InvokeAsync(async () =>
@ -122,8 +150,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// processing incoming JSInterop calls or similar.
for (var i = 0; i < Descriptors.Count; i++)
{
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
var (componentType, domElementSelector, prerendered) = Descriptors[i];
if (!prerendered)
{
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
}
}
catch (Exception ex)
@ -248,10 +279,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
await OnConnectionDownAsync(CancellationToken.None);
await OnCircuitDownAsync();
Renderer.Dispose();
_scope.Dispose();
}));
_scope.Dispose();
Renderer.Dispose();
}
private void AssertInitialized()

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -12,37 +12,66 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitPrerenderer : IComponentPrerenderer
{
private readonly CircuitFactory _circuitFactory;
private static object CircuitHostKey = new object();
public CircuitPrerenderer(CircuitFactory circuitFactory)
private readonly CircuitFactory _circuitFactory;
private readonly CircuitRegistry _registry;
public CircuitPrerenderer(CircuitFactory circuitFactory, CircuitRegistry registry)
{
_circuitFactory = circuitFactory;
_registry = registry;
}
public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
{
var context = prerenderingContext.Context;
var circuitHost = _circuitFactory.CreateCircuitHost(
context,
client: CircuitClientProxy.OfflineClient,
GetFullUri(context.Request),
GetFullBaseUri(context.Request));
var circuitHost = GetOrCreateCircuitHost(context);
// We don't need to unsubscribe because the circuit host object is scoped to this call.
circuitHost.UnhandledException += CircuitHost_UnhandledException;
var renderResult = await circuitHost.PrerenderComponentAsync(
prerenderingContext.ComponentType,
prerenderingContext.Parameters);
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
// reconnect to it from the ComponentsHub. If we keep the circuit/renderer we also need to unsubscribe this error
// handler.
try
circuitHost.Descriptors.Add(new ComponentDescriptor
{
return await circuitHost.PrerenderComponentAsync(
prerenderingContext.ComponentType,
prerenderingContext.Parameters);
ComponentType = prerenderingContext.ComponentType,
Prerendered = true
});
var result = new[] {
$"<!-- M.A.C.Component:{{\"circuitId\":\"{circuitHost.CircuitId}\",\"rendererId\":\"{circuitHost.Renderer.Id}\",\"componentId\":\"{renderResult.ComponentId}\"}} -->",
}.Concat(renderResult.Tokens).Concat(
new[] {
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
});
return new ComponentPrerenderResult(result);
}
private CircuitHost GetOrCreateCircuitHost(HttpContext context)
{
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
{
return (CircuitHost)existingHost;
}
finally
else
{
await circuitHost.DisposeAsync();
var result = _circuitFactory.CreateCircuitHost(
context,
client: new CircuitClientProxy(), // This creates an "offline" client.
GetFullUri(context.Request),
GetFullBaseUri(context.Request));
result.UnhandledException += CircuitHost_UnhandledException;
context.Response.OnCompleted(() =>
{
result.UnhandledException -= CircuitHost_UnhandledException;
_registry.RegisterDisconnectedCircuit(result);
return Task.CompletedTask;
});
context.Items.Add(CircuitHostKey, result);
return result;
}
}

View File

@ -121,6 +121,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Debug.Assert(result, "This operation operates inside of a lock. We expect the previously inspected value to be still here.");
circuitHost.Client.SetDisconnected();
RegisterDisconnectedCircuit(circuitHost);
return true;
}
public void RegisterDisconnectedCircuit(CircuitHost circuitHost)
{
var entryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_options.DisconnectedCircuitRetentionPeriod),
@ -129,7 +135,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
};
DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, entryOptions);
return true;
}
public virtual async Task<CircuitHost> ConnectAsync(string circuitId, IClientProxy clientProxy, string connectionId, CancellationToken cancellationToken)

View File

@ -46,13 +46,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
componentContext.Initialize(client);
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
if (client != CircuitClientProxy.OfflineClient)
if (client.Connected)
{
uriHelper.Initialize(uriAbsolute, baseUriAbsolute, jsRuntime);
uriHelper.AttachJsRuntime(jsRuntime);
uriHelper.InitializeState(
uriAbsolute,
baseUriAbsolute);
}
else
{
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
uriHelper.InitializeState(uriAbsolute, baseUriAbsolute);
}
var rendererRegistry = new RendererRegistry();
@ -87,12 +90,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return circuitHost;
}
private static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
internal static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
{
if (client == CircuitClientProxy.OfflineClient)
if (!client.Connected)
{
// This is the prerendering case.
return Array.Empty<ComponentDescriptor>();
// This is the prerendering case. Descriptors will be registered by the prerenderer.
return new List<ComponentDescriptor>();
}
else
{
@ -106,10 +109,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}
var componentsMetadata = endpoint.Metadata.OfType<ComponentDescriptor>().ToList();
if (componentsMetadata.Count == 0)
{
throw new InvalidOperationException("No component was registered with the component hub.");
}
return componentsMetadata;
}

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class RemoteJSRuntime : JSRuntimeBase
{
private IClientProxy _clientProxy;
private CircuitClientProxy _clientProxy;
internal void Initialize(CircuitClientProxy clientProxy)
{
@ -18,11 +18,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
if (_clientProxy == CircuitClientProxy.OfflineClient)
if (!_clientProxy.Connected)
{
var errorMessage = "JavaScript interop calls cannot be issued while the client is not connected, because the server is not able to interop with the browser at this time. " +
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during periods where the client is not connected.";
throw new InvalidOperationException(errorMessage);
throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
"attempted during prerendering or while the client is disconnected.");
}
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
@ -19,18 +20,12 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
{
internal class RemoteRenderer : HtmlRenderer
{
// The purpose of the timeout is just to ensure server resources are released at some
// point if the client disconnects without sending back an ACK after a render
private const int TimeoutMilliseconds = 60 * 1000;
private readonly int _id;
private readonly IJSRuntime _jsRuntime;
private readonly CircuitClientProxy _client;
private readonly RendererRegistry _rendererRegistry;
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
private readonly ILogger _logger;
private long _nextRenderId = 1;
private bool _disposing = false;
/// <summary>
/// Notifies when a rendering exception occured.
@ -54,11 +49,13 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
_jsRuntime = jsRuntime;
_client = client;
_id = _rendererRegistry.Add(this);
Id = _rendererRegistry.Add(this);
_logger = logger;
}
internal ConcurrentQueue<byte[]> OfflineRenderBatches = new ConcurrentQueue<byte[]>();
internal ConcurrentQueue<PendingRender> PendingRenderBatches = new ConcurrentQueue<PendingRender>();
public int Id { get; }
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
@ -73,7 +70,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
var attachComponentTask = _jsRuntime.InvokeAsync<object>(
"Blazor._internal.attachRootComponentToElement",
_id,
Id,
domElementSelector,
componentId);
CaptureAsyncExceptions(attachComponentTask);
@ -102,13 +99,24 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_disposing = true;
base.Dispose(true);
_rendererRegistry.TryRemove(_id);
while (PendingRenderBatches.TryDequeue(out var entry))
{
entry.CompletionSource.TrySetCanceled();
}
_rendererRegistry.TryRemove(Id);
}
/// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch batch)
{
if (_disposing)
{
// We are being disposed, so do no work.
return Task.FromCanceled<object>(CancellationToken.None);
}
// Note that we have to capture the data as a byte[] synchronously here, because
// SignalR's SendAsync can wait an arbitrary duration before serializing the params.
// The RenderBatch buffer will get reused by subsequent renders, so we need to
@ -126,77 +134,112 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
batchBytes = memoryStream.ToArray();
}
if (!_client.Connected)
{
// Buffer the rendered batches while the client is disconnected. We'll send it down once the client reconnects.
OfflineRenderBatches.Enqueue(batchBytes);
return Task.CompletedTask;
}
Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId);
return WriteBatchBytes(batchBytes);
}
public async Task ProcessBufferedRenderBatches()
{
// The server may discover that the client disconnected while we're attempting to write empty rendered batches.
// Discontinue writing in this event.
while (_client.Connected && OfflineRenderBatches.TryDequeue(out var renderBatch))
{
await WriteBatchBytes(renderBatch);
}
}
private Task WriteBatchBytes(byte[] batchBytes)
{
var renderId = Interlocked.Increment(ref _nextRenderId);
var pendingRenderInfo = new AutoCancelTaskCompletionSource<object>(TimeoutMilliseconds);
_pendingRenders[renderId] = pendingRenderInfo;
var pendingRender = new PendingRender(
renderId,
batchBytes,
new TaskCompletionSource<object>());
// Send the render batch to the client
// If the "send" operation fails (synchronously or asynchronously), abort
// the whole render with that exception
try
{
_client.SendAsync("JS.RenderBatch", _id, renderId, batchBytes).ContinueWith(sendTask =>
{
if (sendTask.IsFaulted)
{
pendingRenderInfo.TrySetException(sendTask.Exception);
}
});
}
catch (Exception syncException)
{
pendingRenderInfo.TrySetException(syncException);
}
// Buffer the rendered batches no matter what. We'll send it down immediately when the client
// is connected or right after the client reconnects.
// When the render is completed (success, fail, or timeout), stop tracking it
return pendingRenderInfo.Task.ContinueWith(task =>
{
_pendingRenders.TryRemove(renderId, out var ignored);
if (task.IsFaulted)
{
UnhandledException?.Invoke(this, task.Exception);
}
});
PendingRenderBatches.Enqueue(pendingRender);
// Fire and forget the initial send for this batch (if connected). Otherwise it will be sent
// as soon as the client reconnects.
var _ = WriteBatchBytesAsync(pendingRender);
return pendingRender.CompletionSource.Task;
}
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
public Task ProcessBufferedRenderBatches()
{
if (_pendingRenders.TryGetValue(renderId, out var pendingRenderInfo))
// All the batches are sent in order based on the fact that SignalR
// provides ordering for the underlying messages and that the batches
// are always in order.
return Task.WhenAll(PendingRenderBatches.Select(b => WriteBatchBytesAsync(b)));
}
private async Task WriteBatchBytesAsync(PendingRender pending)
{
// Send the render batch to the client
// If the "send" operation fails (synchronously or asynchronously) or the client
// gets disconected simply give up. This likely means that
// the circuit went offline while sending the data, so simply wait until the
// client reconnects back or the circuit gets evicted because it stayed
// disconnected for too long.
try
{
if (errorMessageOrNull == null)
if (!_client.Connected)
{
pendingRenderInfo.TrySetResult(null);
}
else
{
pendingRenderInfo.TrySetException(
new RemoteRendererException(errorMessageOrNull));
// If we detect that the client is offline. Simply stop trying to send the payload.
// When the client reconnects we'll resend it.
return;
}
Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId);
await _client.SendAsync("JS.RenderBatch", Id, pending.BatchId, pending.Data);
}
catch (Exception e)
{
Log.SendBatchDataFailed(_logger, e);
}
// We don't have to remove the entry from the list of pending batches if we fail to send it or the client fails to
// acknowledge that it received it. We simply keep it in the queue until we receive another ack from the client for
// a later batch (clientBatchId > thisBatchId) or the circuit becomes disconnected and we ultimately get evicted and
// disposed.
}
public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
{
if (_disposing)
{
// Disposing so don't do work.
return;
}
if (!PendingRenderBatches.TryDequeue(out var entry) || entry.BatchId != incomingBatchId)
{
HandleException(
new InvalidOperationException($"Received a notification for a rendered batch when not expecting it. Batch id '{incomingBatchId}'."));
}
else
{
var message = $"Completing batch {entry.BatchId} " +
errorMessageOrNull == null ? "without error." : "with error.";
_logger.LogDebug(message);
CompleteRender(entry.CompletionSource, errorMessageOrNull);
}
}
private void CompleteRender(TaskCompletionSource<object> pendingRenderInfo, string errorMessageOrNull)
{
if (errorMessageOrNull == null)
{
pendingRenderInfo.TrySetResult(null);
}
else
{
pendingRenderInfo.TrySetException(new RemoteRendererException(errorMessageOrNull));
}
}
internal readonly struct PendingRender
{
public PendingRender(long batchId, byte[] data, TaskCompletionSource<object> completionSource)
{
BatchId = batchId;
Data = data;
CompletionSource = completionSource;
}
public long BatchId { get; }
public byte[] Data { get; }
public TaskCompletionSource<object> CompletionSource { get; }
}
private void CaptureAsyncExceptions(Task task)
@ -215,12 +258,14 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
private static readonly Action<ILogger, string, Exception> _beginUpdateDisplayAsync;
private static readonly Action<ILogger, string, Exception> _bufferingRenderDisconnectedClient;
private static readonly Action<ILogger, string, Exception> _sendBatchDataFailed;
private static class EventIds
{
public static readonly EventId UnhandledExceptionRenderingComponent = new EventId(100, "ExceptionRenderingComponent");
public static readonly EventId BeginUpdateDisplayAsync = new EventId(101, "BeginUpdateDisplayAsync");
public static readonly EventId SkipUpdateDisplayAsync = new EventId(102, "SkipUpdateDisplayAsync");
public static readonly EventId SendBatchDataFailed = new EventId(103, "SendBatchDataFailed");
}
static Log()
@ -239,6 +284,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
LogLevel.Trace,
EventIds.SkipUpdateDisplayAsync,
"Buffering remote render because the client on connection {ConnectionId} is disconnected.");
_sendBatchDataFailed = LoggerMessage.Define<string>(
LogLevel.Information,
EventIds.SendBatchDataFailed,
"Sending data for batch failed: {Message}");
}
public static void SendBatchDataFailed(ILogger logger, Exception exception)
{
_sendBatchDataFailed(logger, exception.Message, exception);
}
public static void UnhandledExceptionRenderingComponent(ILogger logger, Exception exception)

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Interop = Microsoft.AspNetCore.Components.Browser.BrowserUriHelperInterop;
@ -15,6 +16,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public class RemoteUriHelper : UriHelperBase
{
private IJSRuntime _jsRuntime;
private readonly ILogger<RemoteUriHelper> _logger;
public RemoteUriHelper(ILogger<RemoteUriHelper> logger)
{
_logger = logger;
}
public bool HasAttachedJSRuntime => _jsRuntime != null;
/// <summary>
/// Initializes the <see cref="RemoteUriHelper"/>.
@ -22,10 +31,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
public void Initialize(string uriAbsolute, string baseUriAbsolute)
public override void InitializeState(string uriAbsolute, string baseUriAbsolute)
{
SetAbsoluteBaseUri(baseUriAbsolute);
SetAbsoluteUri(uriAbsolute);
base.InitializeState(uriAbsolute, baseUriAbsolute);
TriggerOnLocationChanged();
}
@ -35,23 +43,20 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
public void Initialize(string uriAbsolute, string baseUriAbsolute, IJSRuntime jsRuntime)
internal void AttachJsRuntime(IJSRuntime jsRuntime)
{
if (_jsRuntime != null)
{
throw new InvalidOperationException("JavaScript runtime already initialized.");
}
_jsRuntime = jsRuntime;
Initialize(uriAbsolute, baseUriAbsolute);
_jsRuntime.InvokeAsync<object>(
Interop.EnableNavigationInterception,
typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
}
_logger.LogInformation($"{nameof(RemoteUriHelper)} initialized.");
}
/// <summary>
/// For framework use only.
@ -69,14 +74,21 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService<IUriHelper>();
uriHelper.SetAbsoluteUri(uriAbsolute);
uriHelper._logger.LogDebug($"Location changed to '{uriAbsolute}'.");
uriHelper.TriggerOnLocationChanged();
}
protected override void NavigateToCore(string uri, bool forceLoad)
{
_logger.LogDebug($"Log debug {uri} force load {forceLoad}.");
if (_jsRuntime == null)
{
throw new InvalidOperationException("Navigation is not allowed during prerendering.");
throw new InvalidOperationException("Navigation commands can not be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering or while the client is disconnected.");
}
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
}

View File

@ -4,7 +4,9 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -67,6 +69,16 @@ namespace Microsoft.AspNetCore.Components.Server
public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
{
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
{
var endpointFeature = Context.GetHttpContext().Features.Get<IEndpointFeature>();
var endpoint = endpointFeature?.Endpoint;
_logger.LogInformation($"No components registered in the current endpoint '{endpoint.DisplayName}'.");
// No components preregistered so return. This is totally normal if the components were prerendered.
return null;
}
var circuitHost = _circuitFactory.CreateCircuitHost(
Context.GetHttpContext(),
@ -99,10 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server
{
CircuitHost = circuitHost;
// Dispatch any buffered renders we accumulated during a disconnect.
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
_ = circuitHost.Renderer.ProcessBufferedRenderBatches();
circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException);
circuitHost.SendPendingBatches();
return true;
}
@ -122,6 +132,7 @@ namespace Microsoft.AspNetCore.Components.Server
/// </summary>
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
{
_logger.LogInformation($"Received confirmation for batch {renderId}.");
EnsureCircuitHost().Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
}

View File

@ -11,10 +11,13 @@ namespace Microsoft.AspNetCore.Components.Server
public string Selector { get; set; }
public void Deconstruct(out Type componentType, out string selector)
public bool Prerendered { get; set; }
public void Deconstruct(out Type componentType, out string selector, out bool prerendered)
{
componentType = ComponentType;
selector = Selector;
prerendered = Prerendered;
}
}
}

View File

@ -21,6 +21,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
<Reference Include="Microsoft.Extensions.Logging" />
<Reference Include="Microsoft.AspNetCore.SignalR" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.Caching.Memory" />

View File

@ -0,0 +1,33 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// Represents the result of a prerendering an <see cref="IComponent"/>.
/// </summary>
public sealed class ComponentPrerenderResult
{
private readonly IEnumerable<string> _result;
internal ComponentPrerenderResult(IEnumerable<string> result)
{
_result = result;
}
/// <summary>
/// Writes the prerendering result to the given <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> the results will be written to.</param>
public void WriteTo(TextWriter writer)
{
foreach (var element in _result)
{
writer.Write(element);
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server
@ -16,6 +15,6 @@ namespace Microsoft.AspNetCore.Components.Server
/// </summary>
/// <param name="context">The context in which the prerrendering is happening.</param>
/// <returns><see cref="Task{TResult}"/> that will complete when the prerendering is done.</returns>
Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context);
Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext context);
}
}

View File

@ -39,6 +39,36 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.True(remoteRenderer.Disposed);
}
[Fact]
public async Task DisposeAsync_DisposesRendererWithinSynchronizationContext()
{
// Arrange
var serviceScope = new Mock<IServiceScope>();
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
var circuitHost = TestCircuitHost.Create(
serviceScope.Object,
remoteRenderer);
var component = new DispatcherComponent(circuitHost.Dispatcher);
circuitHost.Renderer.AssignRootComponentId(component);
var original = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
// Act & Assert
try
{
Assert.Null(SynchronizationContext.Current);
await circuitHost.DisposeAsync();
Assert.True(component.Called);
Assert.Null(SynchronizationContext.Current);
}
finally
{
// Not sure if the line above messes up the xunit sync context, so just being cautious here.
SynchronizationContext.SetSynchronizationContext(original);
}
}
[Fact]
public async Task InitializeAsync_InvokesHandlers()
{
@ -190,5 +220,22 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Disposed = true;
}
}
private class DispatcherComponent : ComponentBase, IDisposable
{
public DispatcherComponent(IDispatcher dispatcher)
{
Dispatcher = dispatcher;
}
public IDispatcher Dispatcher { get; }
public bool Called { get; private set; }
public void Dispose()
{
Called = true;
Assert.Same(Dispatcher, SynchronizationContext.Current);
}
}
}
}

View File

@ -1,11 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
@ -13,6 +19,10 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
{
public class CircuitPrerendererTest
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"0\",\"componentId\":\"0\"}} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
// Because CircuitPrerenderer is a point of integration with HttpContext,
// it's not a good candidate for unit testing. The majority of prerendering
// unit tests should be elsewhere in HtmlRendererTests inside the
@ -26,32 +36,40 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
{
// Arrange
var circuitFactory = new TestCircuitFactory();
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory);
var httpContext = new Mock<HttpContext>();
var httpRequest = new Mock<HttpRequest>().SetupAllProperties();
httpContext.Setup(h => h.Request).Returns(httpRequest.Object);
httpRequest.Object.Scheme = "https";
httpRequest.Object.Host = new HostString("example.com", 1234);
httpRequest.Object.Path = "/some/path";
var circuitRegistry = new CircuitRegistry(Options.Create(new CircuitOptions()), Mock.Of<ILogger<CircuitRegistry>>());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterCollection.Empty,
Context = httpContext.Object
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal(new[]
Assert.Equal(string.Join("", new[]
{
"The current URI is ",
"https://example.com:1234/some/path",
" within base URI ",
"https://example.com:1234/"
}, result);
}), GetUnwrappedContent(result));
}
private string GetUnwrappedContent(ComponentPrerenderResult rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer);
return ContentWrapperRegex.Match(writer.ToString())
.Groups["content"].Value
.Replace("\r\n","\n");
}
[Fact]
@ -59,33 +77,33 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
{
// Arrange
var circuitFactory = new TestCircuitFactory();
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory);
var httpContext = new Mock<HttpContext>();
var httpRequest = new Mock<HttpRequest>().SetupAllProperties();
httpContext.Setup(h => h.Request).Returns(httpRequest.Object);
httpRequest.Object.Scheme = "https";
httpRequest.Object.Host = new HostString("example.com", 1234);
httpRequest.Object.PathBase = "/my/dir";
httpRequest.Object.Path = "/some/path";
var circuitRegistry = new CircuitRegistry(Options.Create(new CircuitOptions()), Mock.Of<ILogger<CircuitRegistry>>());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.PathBase = "/my/dir";
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterCollection.Empty,
Context = httpContext.Object
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal(new[]
Assert.Equal(string.Join("", new[]
{
"The current URI is ",
"https://example.com:1234/my/dir/some/path",
" within base URI ",
"https://example.com:1234/my/dir/"
}, result);
}), GetUnwrappedContent(result));
}
class TestCircuitFactory : CircuitFactory
@ -95,8 +113,8 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IUriHelper>(_ =>
{
var uriHelper = new RemoteUriHelper();
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
var uriHelper = new RemoteUriHelper(NullLogger<RemoteUriHelper>.Instance);
uriHelper.InitializeState(uriAbsolute, baseUriAbsolute);
return uriHelper;
});
var serviceScope = serviceCollection.BuildServiceProvider().CreateScope();

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
{
protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider)
{
return GetRemoteRenderer(serviceProvider, CircuitClientProxy.OfflineClient);
return GetRemoteRenderer(serviceProvider, new CircuitClientProxy());
}
[Fact]
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
component.TriggerRender();
// Assert
Assert.Equal(2, renderer.OfflineRenderBatches.Count);
Assert.Equal(2, renderer.PendingRenderBatches.Count);
}
[Fact]
@ -51,15 +51,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
{
// Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderIds = new List<int>();
var renderIds = new List<long>();
var firstBatchTCS = new TaskCompletionSource<object>();
var secondBatchTCS = new TaskCompletionSource<object>();
var thirdBatchTCS = new TaskCompletionSource<object>();
var initialClient = new Mock<IClientProxy>();
initialClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
.Callback((string name, object[] value, CancellationToken token) =>
{
renderIds.Add((int)value[1]);
})
.Returns(Task.CompletedTask);
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
.Returns(firstBatchTCS.Task);
var circuitClient = new CircuitClientProxy(initialClient.Object, "connection0");
var renderer = GetRemoteRenderer(serviceProvider, circuitClient);
var component = new TestComponent(builder =>
@ -68,18 +69,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
builder.AddContent(1, "some text");
builder.CloseElement();
});
var client = new Mock<IClientProxy>();
client.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
.Callback((string name, object[] value, CancellationToken token) =>
{
renderIds.Add((int)value[1]);
})
.Returns(Task.CompletedTask);
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task);
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
renderer.OnRenderCompleted(1, null);
renderer.OnRenderCompleted(2, null);
firstBatchTCS.SetResult(null);
circuitClient.SetDisconnected();
component.TriggerRender();
@ -88,22 +87,174 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
// Act
circuitClient.Transfer(client.Object, "new-connection");
var task = renderer.ProcessBufferedRenderBatches();
foreach (var id in renderIds)
foreach (var id in renderIds.ToArray())
{
renderer.OnRenderCompleted(id, null);
}
await task;
secondBatchTCS.SetResult(null);
thirdBatchTCS.SetResult(null);
// Assert
client.Verify(c => c.SendCoreAsync("JS.RenderBatch", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
Assert.Equal(new long[] { 2, 3, 4 }, renderIds);
Assert.True(task.Wait(3000), "One or more render batches werent acknowledged");
await task;
}
[Fact]
public async Task OnRenderCompletedAsync_ThrowsWhenNoBatchesAreQueued()
{
// Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var firstBatchTCS = new TaskCompletionSource<object>();
var secondBatchTCS = new TaskCompletionSource<object>();
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
offlineClient.SetDisconnected();
var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
RenderFragment initialContent = (builder) =>
{
builder.OpenElement(0, "my element");
builder.AddContent(1, "some text");
builder.CloseElement();
};
var trigger = new Trigger();
var renderIds = new List<long>();
var onlineClient = new Mock<IClientProxy>();
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
// This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(AutoParameterTestComponent.Content)] = initialContent,
[nameof(AutoParameterTestComponent.Trigger)] = trigger
}));
trigger.Component.Content = (builder) =>
{
builder.OpenElement(0, "offline element");
builder.AddContent(1, "offline text");
builder.CloseElement();
};
// This produces an additional batch (id = 3)
trigger.TriggerRender();
var originallyQueuedBatches = renderer.PendingRenderBatches.Count;
// Act
offlineClient.Transfer(onlineClient.Object, "new-connection");
var task = renderer.ProcessBufferedRenderBatches();
var exceptions = new List<Exception>();
renderer.UnhandledException += (sender, e) =>
{
exceptions.Add(e);
};
// Pretend that we missed the ack for the initial batch
renderer.OnRenderCompleted(2, null);
renderer.OnRenderCompleted(3, null);
firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null);
renderer.OnRenderCompleted(3, null);
// Assert
var exception = Assert.Single(exceptions);
}
[Fact]
public async Task ThrowsIfWeReceiveAnOutOfSequenceClientAcknowledge()
{
// Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var firstBatchTCS = new TaskCompletionSource<object>();
var secondBatchTCS = new TaskCompletionSource<object>();
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
offlineClient.SetDisconnected();
var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
RenderFragment initialContent = (builder) =>
{
builder.OpenElement(0, "my element");
builder.AddContent(1, "some text");
builder.CloseElement();
};
var trigger = new Trigger();
var renderIds = new List<long>();
var onlineClient = new Mock<IClientProxy>();
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
// This produces the initial batch (id = 2)
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
ParameterCollection.FromDictionary(new Dictionary<string, object>
{
[nameof(AutoParameterTestComponent.Content)] = initialContent,
[nameof(AutoParameterTestComponent.Trigger)] = trigger
}));
trigger.Component.Content = (builder) =>
{
builder.OpenElement(0, "offline element");
builder.AddContent(1, "offline text");
builder.CloseElement();
};
// This produces an additional batch (id = 3)
trigger.TriggerRender();
var originallyQueuedBatches = renderer.PendingRenderBatches.Count;
// Act
offlineClient.Transfer(onlineClient.Object, "new-connection");
var task = renderer.ProcessBufferedRenderBatches();
var exceptions = new List<Exception>();
renderer.UnhandledException += (sender, e) =>
{
exceptions.Add(e);
};
// Pretend that we missed the ack for the initial batch
renderer.OnRenderCompleted(3, null);
firstBatchTCS.SetResult(null);
secondBatchTCS.SetResult(null);
// Assert
var exception = Assert.Single(exceptions);
}
[Fact]
public async Task PrerendersMultipleComponentsSuccessfully()
{
// Arrange
var serviceProvider = new ServiceCollection().BuildServiceProvider();
var renderer = GetRemoteRenderer(
serviceProvider,
new CircuitClientProxy());
// Act
var first = await renderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty);
var second = await renderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty);
// Assert
Assert.Equal(0, first.ComponentId);
Assert.Equal(1, second.ComponentId);
Assert.Equal(2, renderer.PendingRenderBatches.Count);
}
private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy)
{
var jsRuntime = new Mock<IJSRuntime>();
jsRuntime.Setup(r => r.InvokeAsync<object>(
"Blazor._internal.attachRootComponentToElement",
It.IsAny<int>(),
It.IsAny<string>(),
It.IsAny<int>()))
.ReturnsAsync(Task.FromResult<object>(null));
return new RemoteRenderer(
serviceProvider,
new RendererRegistry(),
Mock.Of<IJSRuntime>(),
jsRuntime.Object,
circuitClientProxy,
Dispatcher,
HtmlEncoder.Default,
@ -113,7 +264,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
private class TestComponent : IComponent
{
private RenderHandle _renderHandle;
private RenderFragment _renderFragment;
private RenderFragment _renderFragment = (builder) =>
{
builder.OpenElement(0, "my element");
builder.AddContent(1, "some text");
builder.CloseElement();
};
public TestComponent()
{
}
public TestComponent(RenderFragment renderFragment)
{
@ -137,5 +297,43 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
Assert.True(task.IsCompletedSuccessfully);
}
}
private class AutoParameterTestComponent : IComponent
{
private RenderHandle _renderHandle;
[Parameter] public RenderFragment Content { get; set; }
[Parameter] public Trigger Trigger { get; set; }
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterCollection parameters)
{
Content = parameters.GetValueOrDefault<RenderFragment>(nameof(Content));
Trigger ??= parameters.GetValueOrDefault<Trigger>(nameof(Trigger));
Trigger.Component = this;
TriggerRender();
return Task.CompletedTask;
}
public void TriggerRender()
{
var task = _renderHandle.Invoke(() => _renderHandle.Render(Content));
Assert.True(task.IsCompletedSuccessfully);
}
}
private class Trigger
{
public AutoParameterTestComponent Component { get; set; }
public void TriggerRender()
{
Component.TriggerRender();
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
@ -25,10 +26,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
_serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost;
}
protected override void InitializeAsyncCore()
public override async Task InitializeAsync()
{
await base.InitializeAsync();
Navigate("/", noReload: false);
WaitUntilLoaded();
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("blazorpack")));
}
[Fact]
@ -161,10 +166,23 @@ window.Blazor._internal.forceCloseConnection();");
_ => element.Text != currentValue);
}
private void WaitUntilLoaded()
[Fact]
public void RendersContinueAfterPrerendering()
{
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
driver => driver.FindElement(By.TagName("app")).Text != "Loading...");
Browser.FindElement(By.LinkText("Greeter")).Click();
Browser.Equal("Hello Guest", () => Browser.FindElement(By.ClassName("greeting")).Text);
}
[Fact]
public void ErrorsStopTheRenderingProcess()
{
Browser.FindElement(By.LinkText("Error")).Click();
Browser.Equal("Error", () => Browser.FindElement(By.CssSelector("h1")).Text);
Browser.FindElement(By.Id("cause-error")).Click();
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("Connection disconnected.")));
}
}
}

View File

@ -459,8 +459,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public void CanUseJsInteropForRefElementsDuringOnAfterRender()
{
var appElement = MountTestComponent<AfterRenderInteropComponent>();
var inputElement = appElement.FindElement(By.TagName("input"));
Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
Browser.Equal("Value set after render", () => Browser.FindElement(By.TagName("input")).GetAttribute("value"));
}
[Fact]

View File

@ -1,5 +1,12 @@
@using Microsoft.AspNetCore.Components;
<!--
Configuring this stuff here is temporary. Later we'll move the app config
into Startup.cs, and it won't be necessary to specify AppAssembly.
-->
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
<CascadingValue Value="Name" Name="Name" IsFixed=true>
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
</CascadingValue>
@functions{
[Parameter] public string Name { get; set; }
}

View File

@ -0,0 +1,19 @@
@page "/error"
@using Microsoft.AspNetCore.Components
<h1>Error</h1>
@if(ShouldCauseError)
{
throw new InvalidOperationException("Exception while rendering");
}
else
{
<button id="cause-error" onclick="@CauseError">Cause error</button>
}
@functions {
public bool ShouldCauseError { get; set; }
void CauseError()
{
ShouldCauseError = true;
}
}

View File

@ -0,0 +1,8 @@
@page "/greeter"
@using Microsoft.AspNetCore.Components
<h1>Greeter</h1>
<p class="greeting">Hello @Name</p>
@functions {
[CascadingParameter(Name=nameof(Name))] public string Name { get; set; }
}

View File

@ -27,6 +27,16 @@
<span class="oi oi-list-rich" aria-hidden="true"></span> Ticker
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="greeter">
<span class="oi oi-list-rich" aria-hidden="true"></span> Greeter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="error">
<span class="oi oi-list-rich" aria-hidden="true"></span> Error
</NavLink>
</li>
</ul>
</div>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -7,6 +7,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<ProjectReference Include="..\ComponentsApp.App\ComponentsApp.App.csproj" />

View File

@ -1,4 +1,4 @@
@page "{*clientroutes}"
@page
@using ComponentsApp.App
<!DOCTYPE html>
@ -7,13 +7,15 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width">
<title>Razor Components</title>
<base href="/" />
<base href="~/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<App>())</app>
<app>@(await Html.RenderComponentAsync<App>(new { Name="Guest" }))</app>
<script src="_framework/components.server.js"></script>
</body>
</html>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
@ -35,7 +36,9 @@ namespace ComponentsApp.Server
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapComponentHub<App.App>("app");
endpoints.MapControllers();
endpoints.MapComponentHub();
endpoints.MapFallbackToPage("/Index");
});
}
}

View File

@ -0,0 +1,13 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug"
},
"Console": {
"LogLevel": {
"Default": "Debug"
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}

View File

@ -1,4 +1,4 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,5 +1,6 @@
@page
@using BasicTestApp.RouterTest
@using Microsoft.AspNetCore.Mvc.ViewFeatures
<!DOCTYPE html>
<html>
<head>

View File

@ -12,6 +12,7 @@
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
</ItemGroup>
<ItemGroup>

View File

@ -1,10 +1,10 @@
{
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
"System": "Debug",
"Microsoft": "Debug"
}
}
}

View File

@ -3,7 +3,7 @@
"devDependencies": {
"jest": "^23.6.0",
"merge": "^1.2.1",
"puppeteer": "^1.13.0"
"puppeteer": "^1.14.0"
},
"dependencies": {},
"scripts": {

View File

@ -2826,10 +2826,10 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer@^1.13.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.13.0.tgz#187ccf5ed5caf08ed1291b262d033cc364bf88ab"
integrity sha512-LUXgvhjfB/P6IOUDAKxOcbCz9ISwBLL9UpKghYrcBDwrOGx1m60y0iN2M64mdAUbT4+7oZM5DTxOW7equa2fxQ==
puppeteer@^1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339"
integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw==
dependencies:
debug "^4.1.0"
extract-zip "^1.6.6"

View File

@ -0,0 +1,11 @@
<!-- This file is automatically generated. -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Mvc.Components.Prerendering.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public static partial class HtmlHelperComponentPrerenderingExtensions
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class HtmlContentPrerenderComponentResultAdapter : IHtmlContent
{
private ComponentPrerenderResult _result;
public HtmlContentPrerenderComponentResultAdapter(ComponentPrerenderResult result)
{
_result = result;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
_result.WriteTo(writer);
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentPrerenderingExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(null);
}
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
var httpContext = htmlHelper.ViewContext.HttpContext;
var serviceProvider = httpContext.RequestServices;
var prerenderer = serviceProvider.GetService<IComponentPrerenderer>();
if (prerenderer == null)
{
throw new InvalidOperationException($"No '{typeof(IComponentPrerenderer).Name}' implementation has been registered in the dependency injection container. " +
$"This typically means a call to 'services.AddRazorComponents()' is missing in 'Startup.ConfigureServices'.");
}
var parametersCollection = parameters == null ?
ParameterCollection.Empty :
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
var result = await prerenderer.PrerenderComponentAsync(
new ComponentPrerenderingContext
{
ComponentType = typeof(TComponent),
Parameters = parametersCollection,
Context = httpContext
});
return new HtmlContentPrerenderComponentResultAdapter(result);
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core MVC component interactive rendering features. Contains types to integrate server-side rendered components into MVC Views and Pages.
</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;aspnetcoremvc</PackageTags>
<IsAspNetCoreApp>false</IsAspNetCoreApp>
<IsShippingPackage>true</IsShippingPackage>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,457 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class HtmlHelperComponentExtensionsTests
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"0\",\"componentId\":\"0\"}} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
[Fact]
public async Task PrerenderComponentAsync_ThrowsInvalidOperationException_IfNoPrerendererHasBeenRegistered()
{
// Arrange
var helper = CreateHelper(null, s => { });
var writer = new StringWriter();
var expectedmessage = $"No 'IComponentPrerenderer' implementation has been registered in the dependency injection container. " +
$"This typically means a call to 'services.AddRazorComponents()' is missing in 'Startup.ConfigureServices'.";
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<TestComponent>());
// Assert
Assert.Equal(expectedmessage, exception.Message);
}
[Fact]
public async Task CanRender_ParameterlessComponent()
{
// Arrange
var helper = CreateHelper();
// Act
var result = await helper.RenderComponentAsync<TestComponent>();
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal("<h1>Hello world!</h1>", unwrappedContent);
}
[Fact]
public async Task CanRender_ComponentWithParametersObject()
{
// Arrange
var helper = CreateHelper();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new
{
Name = "Guest"
});
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal("<p>Hello Guest!</p>", unwrappedContent);
}
[Fact]
public async Task CanCatch_ComponentWithSynchronousException()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = false
}));
// Assert
Assert.Equal("Threw an exception synchronously", exception.Message);
}
[Fact]
public async Task CanCatch_ComponentWithAsynchronousException()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = true
}));
// Assert
Assert.Equal("Threw an exception asynchronously", exception.Message);
}
[Fact]
public async Task Rendering_ComponentWithJsInteropThrows()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
JsInterop = true
}));
// Assert
Assert.Equal("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
"attempted during prerendering or while the client is disconnected.",
exception.Message);
}
[Fact]
public async Task UriHelperRedirect_ThrowsInvalidOperationException()
{
// Arrange
var ctx = new DefaultHttpContext();
ctx.Request.Scheme = "http";
ctx.Request.Host = new HostString("localhost");
ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value");
var helper = CreateHelper(ctx);
var writer = new StringWriter();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
}));
Assert.Equal("Navigation commands can not be issued at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering or while the client is disconnected.",
exception.Message);
}
[Fact]
public async Task CanRender_AsyncComponent()
{
// Arrange
var helper = CreateHelper();
var expectedContent = @"<table>
<thead>
<tr>
<th>Date</th>
<th>Summary</th>
<th>F</th>
<th>C</th>
</tr>
</thead>
<tbody>
<tr>
<td>06/05/2018</td>
<td>Freezing</td>
<td>33</td>
<td>33</td>
</tr>
<tr>
<td>07/05/2018</td>
<td>Bracing</td>
<td>57</td>
<td>57</td>
</tr>
<tr>
<td>08/05/2018</td>
<td>Freezing</td>
<td>9</td>
<td>9</td>
</tr>
<tr>
<td>09/05/2018</td>
<td>Balmy</td>
<td>4</td>
<td>4</td>
</tr>
<tr>
<td>10/05/2018</td>
<td>Chilly</td>
<td>29</td>
<td>29</td>
</tr>
</tbody>
</table>";
// Act
var result = await helper.RenderComponentAsync<AsyncComponent>();
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), unwrappedContent);
}
private string GetUnwrappedContent(IHtmlContent rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer, HtmlEncoder.Default);
return ContentWrapperRegex.Match(writer.ToString())
.Groups["content"].Value
.Replace("\r\n", "\n");
}
private class TestComponent : IComponent
{
private RenderHandle _renderHandle;
public void Configure(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterCollection parameters)
{
_renderHandle.Render(builder =>
{
builder.OpenElement(1, "h1");
builder.AddContent(2, "Hello world!");
builder.CloseElement();
});
return Task.CompletedTask;
}
}
private class RedirectComponent : ComponentBase
{
[Inject] IUriHelper UriHelper { get; set; }
[Parameter] public string RedirectUri { get; set; }
[Parameter] public bool Force { get; set; }
protected override void OnInit()
{
UriHelper.NavigateTo(RedirectUri, Force);
}
}
private class ExceptionComponent : ComponentBase
{
[Parameter] bool IsAsync { get; set; }
[Parameter] bool JsInterop { get; set; }
[Inject] IJSRuntime JsRuntime { get; set; }
protected override async Task OnParametersSetAsync()
{
if (JsInterop)
{
await JsRuntime.InvokeAsync<int>("window.alert", "Interop!");
}
if (!IsAsync)
{
throw new InvalidOperationException("Threw an exception synchronously");
}
else
{
await Task.Yield();
throw new InvalidOperationException("Threw an exception asynchronously");
}
}
}
private class GreetingComponent : ComponentBase
{
[Parameter] public string Name { get; set; }
protected override void OnParametersSet()
{
base.OnParametersSet();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(1, "p");
builder.AddContent(2, $"Hello {Name}!");
builder.CloseElement();
}
}
private class AsyncComponent : ComponentBase
{
private static WeatherRow[] _weatherData = new[]
{
new WeatherRow
{
DateFormatted = "06/05/2018",
TemperatureC = 1,
Summary = "Freezing",
TemperatureF = 33
},
new WeatherRow
{
DateFormatted = "07/05/2018",
TemperatureC = 14,
Summary = "Bracing",
TemperatureF = 57
},
new WeatherRow
{
DateFormatted = "08/05/2018",
TemperatureC = -13,
Summary = "Freezing",
TemperatureF = 9
},
new WeatherRow
{
DateFormatted = "09/05/2018",
TemperatureC = -16,
Summary = "Balmy",
TemperatureF = 4
},
new WeatherRow
{
DateFormatted = "10/05/2018",
TemperatureC = 2,
Summary = "Chilly",
TemperatureF = 29
}
};
public class WeatherRow
{
public string DateFormatted { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF { get; set; }
}
public WeatherRow[] RowsToDisplay { get; set; }
protected override async Task OnParametersSetAsync()
{
// Simulate an async workflow.
await Task.Yield();
RowsToDisplay = _weatherData;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "table");
builder.AddMarkupContent(1, "\n");
builder.OpenElement(2, "thead");
builder.AddMarkupContent(3, "\n");
builder.OpenElement(4, "tr");
builder.AddMarkupContent(5, "\n");
builder.OpenElement(6, "th");
builder.AddContent(7, "Date");
builder.CloseElement();
builder.AddMarkupContent(8, "\n");
builder.OpenElement(9, "th");
builder.AddContent(10, "Summary");
builder.CloseElement();
builder.AddMarkupContent(11, "\n");
builder.OpenElement(12, "th");
builder.AddContent(13, "F");
builder.CloseElement();
builder.AddMarkupContent(14, "\n");
builder.OpenElement(15, "th");
builder.AddContent(16, "C");
builder.CloseElement();
builder.AddMarkupContent(17, "\n");
builder.CloseElement();
builder.AddMarkupContent(18, "\n");
builder.CloseElement();
builder.AddMarkupContent(19, "\n");
builder.OpenElement(20, "tbody");
builder.AddMarkupContent(21, "\n");
if (RowsToDisplay != null)
{
foreach (var element in RowsToDisplay)
{
builder.OpenElement(22, "tr");
builder.AddMarkupContent(23, "\n");
builder.OpenElement(24, "td");
builder.AddContent(25, element.DateFormatted);
builder.CloseElement();
builder.AddMarkupContent(26, "\n");
builder.OpenElement(27, "td");
builder.AddContent(28, element.Summary);
builder.CloseElement();
builder.AddMarkupContent(29, "\n");
builder.OpenElement(30, "td");
builder.AddContent(31, element.TemperatureF);
builder.CloseElement();
builder.AddMarkupContent(32, "\n");
builder.OpenElement(33, "td");
builder.AddContent(34, element.TemperatureF);
builder.CloseElement();
builder.AddMarkupContent(35, "\n");
builder.CloseElement();
builder.AddMarkupContent(36, "\n");
}
}
builder.CloseElement();
builder.AddMarkupContent(37, "\n");
builder.CloseElement();
}
}
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(HtmlEncoder.Default);
configureServices = configureServices ?? (s => s.AddRazorComponents());
configureServices?.Invoke(services);
var helper = new Mock<IHtmlHelper>();
var context = ctx ?? new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
context.Request.PathBase = "/base";
context.Request.Path = "/path";
context.Request.QueryString = QueryString.FromUriComponent("?query=value");
helper.Setup(h => h.ViewContext)
.Returns(new ViewContext()
{
HttpContext = context
});
return helper.Object;
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\shared\Mvc.Views.TestCommon\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj" />
<ProjectReference Include="..\..\shared\Mvc.TestDiagnosticListener\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj" />
</ItemGroup>
</Project>

View File

@ -1,20 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components.Server
{
public partial class ComponentPrerenderingContext
{
public ComponentPrerenderingContext() { }
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.HttpContext Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Components.ParameterCollection Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial interface IComponentPrerenderer
{
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> PrerenderComponentAsync(Microsoft.AspNetCore.Components.Server.ComponentPrerenderingContext context);
}
}
namespace Microsoft.AspNetCore.Mvc
{
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
@ -993,9 +979,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
}
public static partial class HtmlHelperRazorComponentExtensions
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
public partial class HtmlHelper<TModel> : Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel>
{

View File

@ -4,7 +4,6 @@
using System;
using System.Buffers;
using System.Linq;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
@ -206,7 +205,7 @@ namespace Microsoft.Extensions.DependencyInjection
//
// Component prerendering
//
services.TryAddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<IUriHelper, HttpUriHelper>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();

View File

@ -4,7 +4,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
@ -22,14 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
public static Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(null);
return htmlHelper.RenderStaticComponentAsync<TComponent>(null);
}
/// <summary>
@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
public static async Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
object parameters) where TComponent : IComponent
{
@ -50,14 +49,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
var httpContext = htmlHelper.ViewContext.HttpContext;
var serviceProvider = httpContext.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<IComponentPrerenderer>();
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var result = await prerenderer.PrerenderComponentAsync(new ComponentPrerenderingContext
{
Context = httpContext,
ComponentType = typeof(TComponent),
Parameters = parameters == null ? ParameterCollection.Empty : ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))
});
var parametersCollection = parameters == null ?
ParameterCollection.Empty :
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
httpContext,
typeof(TComponent));
return new ComponentHtmlContent(result);
}

View File

@ -3,57 +3,17 @@
using System;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class HttpUriHelper : UriHelperBase
{
private HttpContext _context;
public HttpUriHelper()
{
}
public void InitializeState(HttpContext context)
{
_context = context;
InitializeState();
}
protected override void InitializeState()
{
if (_context == null)
{
throw new InvalidOperationException($"'{typeof(HttpUriHelper)}' not initialized.");
}
SetAbsoluteBaseUri(GetContextBaseUri());
SetAbsoluteUri(GetFullUri());
}
private string GetFullUri()
{
var request = _context.Request;
return UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
request.Path,
request.QueryString);
}
private string GetContextBaseUri()
{
var request = _context.Request;
return UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
}
protected override void NavigateToCore(string uri, bool forceLoad)
{
// For now throw as we don't have a good way of aborting the request from here.
throw new InvalidOperationException(
"Redirects are not supported on a prerendering environment.");
throw new InvalidOperationException("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering.");
}
}
}

View File

@ -1,39 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
{
internal class MvcRazorComponentPrerenderer : IComponentPrerenderer
{
private readonly HtmlEncoder _encoder;
public MvcRazorComponentPrerenderer(HtmlEncoder encoder)
{
_encoder = encoder;
}
public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context)
{
var dispatcher = Renderer.CreateDefaultDispatcher();
var parameters = context.Parameters;
// This shouldn't be moved to the constructor as we want a request scoped service.
var helper = (HttpUriHelper)context.Context.RequestServices.GetRequiredService<IUriHelper>();
helper.InitializeState(context.Context);
using (var htmlRenderer = new HtmlRenderer(context.Context.RequestServices, _encoder.Encode, dispatcher))
{
return await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
context.ComponentType,
parameters));
}
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
{
internal class StaticComponentRenderer
{
private readonly HtmlEncoder _encoder;
private bool _initialized = false;
public StaticComponentRenderer(HtmlEncoder encoder)
{
_encoder = encoder;
}
public async Task<IEnumerable<string>> PrerenderComponentAsync(
ParameterCollection parameters,
HttpContext httpContext,
Type componentType)
{
var dispatcher = Renderer.CreateDefaultDispatcher();
InitializeUriHelper(httpContext);
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, _encoder.Encode, dispatcher))
{
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
componentType,
parameters));
return result.Tokens;
}
}
private void InitializeUriHelper(HttpContext httpContext)
{
// We don't know here if we are dealing with the default HttpUriHelper registered
// by MVC or with the RemoteUriHelper registered by AddComponents.
// This might not be the first component in the request we are rendering, so
// we need to check if we already initialized the uri helper in this request.
if (!_initialized)
{
_initialized = true;
var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService<IUriHelper>();
helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request));
}
}
private string GetFullUri(HttpRequest request)
{
return UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
request.Path,
request.QueryString);
}
private string GetContextBaseUri(HttpRequest request)
{
var result = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
// PathBase may be "/" or "/some/thing", but to be a well-formed base URI
// it has to end with a trailing slash
return result.EndsWith("/") ? result : result += "/";
}
}
}

View File

@ -7,7 +7,6 @@ using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderComponentAsync<TestComponent>();
var result = await helper.RenderStaticComponentAsync<TestComponent>();
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -45,7 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new
var result = await helper.RenderStaticComponentAsync<GreetingComponent>(new
{
Name = "Steve"
});
@ -63,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{
IsAsync = false
}));
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{
IsAsync = true
}));
@ -95,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{
JsInterop = true
}));
@ -122,16 +121,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
}));
Assert.Equal("Redirects are not supported on a prerendering environment.", exception.Message);
Assert.Equal("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering.",
exception.Message);
}
[Fact]
public async Task CanRender_AsyncComponent()
{
@ -182,7 +182,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>";
// Act
var result = await helper.RenderComponentAsync<AsyncComponent>();
var result = await helper.RenderStaticComponentAsync<AsyncComponent>();
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<IJSRuntime,UnsupportedJavaScriptRuntime>();
services.AddSingleton<IUriHelper,HttpUriHelper>();
services.AddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
services.AddSingleton<StaticComponentRenderer>();
configureServices?.Invoke(services);

View File

@ -317,6 +317,12 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpWebSite", "test\WebSi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{A8C3066F-E80D-4E03-9962-741B551B8FBC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mvc.Components.Prerendering", "Mvc.Components.Prerendering", "{45CE788D-4B69-4F83-981C-F43D8F15B0F1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering", "Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", "{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering.Test", "Mvc.Components.Prerendering\test\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj", "{C6F3BCE6-1EFD-4360-932B-B98573E78926}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -1809,6 +1815,30 @@ Global
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|x86.ActiveCfg = Release|Any CPU
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|x86.Build.0 = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|x86.ActiveCfg = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|x86.Build.0 = Debug|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Any CPU.Build.0 = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|x86.ActiveCfg = Release|Any CPU
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|x86.Build.0 = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|x86.ActiveCfg = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|x86.Build.0 = Debug|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Any CPU.Build.0 = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|x86.ActiveCfg = Release|Any CPU
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1938,6 +1968,8 @@ Global
{F99CAC82-C96E-41F4-AA28-1BBBD09C447A} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
{65E98187-96FB-4FCD-94A3-F8048C2F13F1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A8C3066F-E80D-4E03-9962-741B551B8FBC} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
{C6F3BCE6-1EFD-4360-932B-B98573E78926} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}

View File

@ -7,6 +7,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />

View File

@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Razor.Runtime" />
<Reference Include="Microsoft.Extensions.WebEncoders" />

View File

@ -478,6 +478,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Act
var response = await Client.GetStringAsync("Home/GetAssemblyPartData");
var assemblyParts = JsonConvert.DeserializeObject<IList<string>>(response);
var expected = new[]
{
"BasicWebSite",
"Microsoft.AspNetCore.Components.Server",
"Microsoft.AspNetCore.Mvc.Components.Prerendering",
"Microsoft.AspNetCore.SpaServices",
"Microsoft.AspNetCore.SpaServices.Extensions",
"Microsoft.AspNetCore.Mvc.TagHelpers",
"Microsoft.AspNetCore.Mvc.Razor",
};
// Assert
//

View File

@ -4,11 +4,12 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Parser.Html;
using BasicWebSite;
using BasicWebSite.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -16,6 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class ComponentRenderingFunctionalTests : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"\\d+\",\"componentId\":\"\\d+\"}} -->(?<content>.*)<!-- M.A.C.Component: \\d+ -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
{
Factory = fixture;
@ -35,14 +40,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
}
[Fact]
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
public async Task Renders_BasicComponent_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components");
@ -50,14 +56,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
}
[Fact]
public async Task Renders_RoutingComponent()
{
// Arrange & Act
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components/routable");
@ -65,14 +71,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
}
[Fact]
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components/routable");
@ -80,14 +87,46 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
}
[Fact]
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer()
public async Task Renders_BasicComponentInteractive_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components/false");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("<p>Hello world!</p>", "Greetings", content, unwrap: true);
}
[Fact]
public async Task Renders_RoutingComponentInteractive_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components/routable/false");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content, unwrap: true);
}
[Fact]
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
var response = await client.GetAsync("http://localhost/components/throws");
@ -103,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
// Arrange & Act
var expectedHtml = @"
<h1>Weather forecast</h1>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@ -150,7 +189,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
</tr>
</tbody>
</table>
";
var client = CreateClient(Factory);
var response = await client.GetAsync("http://localhost/components");
@ -162,14 +200,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
AssertComponent(expectedHtml, "FetchData", content);
}
private void AssertComponent(string expectedConent, string divId, string responseContent)
private void AssertComponent(string expectedContent, string divId, string responseContent, bool unwrap = false)
{
var parser = new HtmlParser();
var htmlDocument = parser.Parse(responseContent);
var div = htmlDocument.Body.QuerySelector($"#{divId}");
var content = unwrap ? GetUnwrappedContent(div.InnerHtml) : div.InnerHtml;
Assert.Equal(
expectedConent.Replace("\r\n","\n"),
div.InnerHtml.Replace("\r\n","\n"));
expectedContent.Replace("\r\n","\n"),
content.Replace("\r\n","\n"));
}
private string GetUnwrappedContent(string rawResult)
{
return ContentWrapperRegex.Match(rawResult)
.Groups["content"].Value
.Replace("\r\n", "\n");
}
// A simple delegating handler used in setting up test services so that we can configure
@ -178,16 +224,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
}
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture, Action<IWebHostBuilder> configure = null)
private HttpClient CreateClient(WebApplicationFactory<BasicWebSite.StartupWithoutEndpointRouting> fixture)
{
var loopHandler = new LoopHttpHandler();
var client = fixture
.WithWebHostBuilder(builder =>
{
configure?.Invoke(builder);
builder.ConfigureServices(ConfigureTestWeatherForecastService);
})
.WithWebHostBuilder(builder => builder.ConfigureServices(ConfigureTestWeatherForecastService))
.CreateClient();
// We configure the inner handler with a handler to this TestServer instance so that calls to the

View File

@ -16,7 +16,7 @@
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Localization.Routing" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers
@ -50,11 +51,16 @@ namespace BasicWebSite.Controllers
}
};
[HttpGet("/components")]
[HttpGet("/components/{component}")]
public IActionResult Index()
[HttpGet("/components/{staticPrerender=true}")]
[HttpGet("/components/routable/{staticPrerender=true}")]
public IActionResult Index(bool staticPrerender)
{
return View();
// Override the path so that the router finds the RoutedPage component
// as the client router doesn't support optional parameters.
Request.Path = Request.Path.StartsWithSegments("/components/routable") ?
PathString.FromUriComponent("/components/routable") : Request.Path;
return View(staticPrerender);
}
[HttpGet("/WeatherData")]

View File

@ -1,13 +1,35 @@
@using BasicWebSite.RazorComponents;
@model bool;
<h1>Razor components</h1>
<div id="Greetings">
@(await Html.RenderComponentAsync<Greetings>())
@if (Model)
{
@(await Html.RenderStaticComponentAsync<Greetings>())
}
else
{
@(await Html.RenderComponentAsync<Greetings>())
}
</div>
<div id="FetchData">
@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
@if (Model)
{
@(await Html.RenderStaticComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
}
else
{
@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
}
</div>
<div id="Routing">
@(await Html.RenderComponentAsync<RouterContainer>())
@if (Model)
{
@(await Html.RenderStaticComponentAsync<RouterContainer>());
}
else
{
@(await Html.RenderComponentAsync<RouterContainer>());
}
</div>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -31,7 +31,7 @@
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Azure\AzureAD\Authentication.AzureADB2C.UI\src\Microsoft.AspNetCore.Authentication.AzureADB2C.UI.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Components\src\Microsoft.AspNetCore.Components.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Browser\src\Microsoft.AspNetCore.Components.Browser.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Identity\EntityFrameworkCore\src\Microsoft.AspNetCore.Identity.EntityFrameworkCore.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Identity\UI\src\Microsoft.AspNetCore.Identity.UI.csproj" />
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />

View File

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Server" Version="${MicrosoftAspNetCoreComponentsServerPackageVersion}" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" Version="${MicrosoftAspNetCoreMvcComponentsPrerenderingPackageVersion}" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="${MicrosoftAspNetCoreMvcNewtonsoftJsonPackageVersion}" />
</ItemGroup>

View File

@ -1,4 +1,4 @@
@page "/{*clientPath}"
@page "/"
@namespace RazorComponentsWeb_CSharp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -53,7 +53,8 @@ namespace RazorComponentsWeb_CSharp
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapComponentHub<App>("app");
endpoints.MapComponentHub();
endpoints.MapFallbackToPage("/Host");
});
}
}

View File

@ -1,4 +1,4 @@
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -16,6 +17,7 @@ using Xunit.Abstractions;
namespace Templates.Test.Helpers
{
[DebuggerDisplay("{ToString(),nq}")]
public class AspNetProcess : IDisposable
{
private const string ListeningMessagePrefix = "Now listening on: ";
@ -142,5 +144,24 @@ namespace Templates.Test.Helpers
_httpClient.Dispose();
Process.Dispose();
}
public override string ToString()
{
var result = "";
result += Process != null ? "Active: " : "Inactive";
if (Process != null)
{
if (!Process.HasExited)
{
result += $"(Listening on {_listeningUri.OriginalString}) PID: {Process.Id}";
}
else
{
result += "(Already finished)";
}
}
return result;
}
}
}

View File

@ -75,6 +75,8 @@ namespace Templates.Test.Helpers
public int ExitCode => _process.ExitCode;
public object Id => _process.Id;
public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary<string, string> envVars = null)
{
var startInfo = new ProcessStartInfo(command, args)

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@ -15,6 +16,7 @@ using Xunit.Sdk;
namespace Templates.Test.Helpers
{
[DebuggerDisplay("{ToString(),nq}")]
public class Project
{
public const string DefaultFramework = "netcoreapp3.0";
@ -384,5 +386,7 @@ namespace Templates.Test.Helpers
}
}
}
public override string ToString() => $"{ProjectName}: {TemplateOutputDir}";
}
}

Some files were not shown because too many files have changed in this diff Show More