diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index ba4bf03987ad..2dc800b91049 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -96,6 +96,7 @@ + diff --git a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs index 878e2a3380f0..cdb2d7aecdcc 100644 --- a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs +++ b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.0.cs @@ -72,7 +72,7 @@ public partial class WebAssemblyUriHelper : Microsoft.AspNetCore.Components.Serv { 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) { } diff --git a/src/Components/Blazor/Blazor/src/Services/WebAssemblyUriHelper.cs b/src/Components/Blazor/Blazor/src/Services/WebAssemblyUriHelper.cs index cb9b068fe562..ecbdf7a92d40 100644 --- a/src/Components/Blazor/Blazor/src/Services/WebAssemblyUriHelper.cs +++ b/src/Components/Blazor/Blazor/src/Services/WebAssemblyUriHelper.cs @@ -25,11 +25,7 @@ internal WebAssemblyUriHelper() { } - /// - /// Called to initialize BaseURI and current URI before those values are used the first time. - /// Override this method to dynamically calculate those values. - /// - protected override void InitializeState() + protected override void EnsureInitialized() { WebAssemblyJSRuntime.Instance.Invoke( Interop.EnableNavigationInterception, @@ -40,8 +36,7 @@ protected override void InitializeState() // client-side (Mono) use, so it's OK to rely on synchronicity here. var baseUri = WebAssemblyJSRuntime.Instance.Invoke(Interop.GetBaseUri); var uri = WebAssemblyJSRuntime.Instance.Invoke(Interop.GetLocationHref); - SetAbsoluteBaseUri(baseUri); - SetAbsoluteUri(uri); + InitializeState(uri, baseUri); } /// diff --git a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs index fc234e267c13..c14c54fae333 100644 --- a/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs +++ b/src/Components/Blazor/Build/test/RazorIntegrationTestBase.cs @@ -452,7 +452,7 @@ public void AttachComponent(IComponent component) protected override void HandleException(Exception exception) { - throw new NotImplementedException(); + ExceptionDispatchInfo.Capture(exception).Throw(); } protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) diff --git a/src/Components/Browser.JS/package.json b/src/Components/Browser.JS/package.json index 12001807b781..44a1397a996b 100644 --- a/src/Components/Browser.JS/package.json +++ b/src/Components/Browser.JS/package.json @@ -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" } diff --git a/src/Components/Browser.JS/src/.eslintrc.js b/src/Components/Browser.JS/src/.eslintrc.js new file mode 100644 index 000000000000..0d1a71b45d48 --- /dev/null +++ b/src/Components/Browser.JS/src/.eslintrc.js @@ -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" + } +}; diff --git a/src/Components/Browser.JS/src/Boot.Server.ts b/src/Components/Browser.JS/src/Boot.Server.ts index 799071d9c040..74c2c295a366 100644 --- a/src/Components/Browser.JS/src/Boot.Server.ts +++ b/src/Components/Browser.JS/src/Boot.Server.ts @@ -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 { + + // 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( - 'StartCircuit', - uriHelperFunctions.getLocationHref(), - uriHelperFunctions.getBaseURI() - ); - - window['Blazor'].reconnect = async () => { - const reconnection = await initializeConnection(circuitHandlers); - if (!(await reconnection.invoke('ConnectCircuit', circuitId))) { + + const circuit = await startCircuit(initialConnection); + + if (!circuit) { + logger.log(LogLevel.Information, 'No preregistered components to render.'); + } + + const reconnect = async (): Promise => { + 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 { +async function initializeConnection(circuitHandlers: CircuitHandler[], logger: ILogger): Promise { 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(); } } diff --git a/src/Components/Browser.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts b/src/Components/Browser.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts index 47d206a89ed0..046527ee7a59 100644 --- a/src/Components/Browser.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts +++ b/src/Components/Browser.JS/src/Platform/Circuits/AutoReconnectCircuitHandler.ts @@ -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{ + public delay(): Promise { return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval)); } - async onConnectionDown() : Promise { + public async onConnectionDown(): Promise { 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); } } diff --git a/src/Components/Browser.JS/src/Platform/Circuits/CircuitHandler.ts b/src/Components/Browser.JS/src/Platform/Circuits/CircuitHandler.ts index ad8a8b75cc27..ab52e0840db8 100644 --- a/src/Components/Browser.JS/src/Platform/Circuits/CircuitHandler.ts +++ b/src/Components/Browser.JS/src/Platform/Circuits/CircuitHandler.ts @@ -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; } diff --git a/src/Components/Browser.JS/src/Platform/Circuits/CircuitManager.ts b/src/Components/Browser.JS/src/Platform/Circuits/CircuitManager.ts new file mode 100644 index 000000000000..b588dfce9dde --- /dev/null +++ b/src/Components/Browser.JS/src/Platform/Circuits/CircuitManager.ts @@ -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 { + return reconnection.invoke('ConnectCircuit', this.circuitId); + } +} + + +export function discoverPrerenderedCircuits(document: Document): CircuitDescriptor[] { + const commentPairs = resolveCommentPairs(document); + const discoveredCircuits = new Map(); + 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 { + const result = await connection.invoke('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}`); +} diff --git a/src/Components/Browser.JS/src/Platform/Circuits/ComponentDescriptor.ts b/src/Components/Browser.JS/src/Platform/Circuits/ComponentDescriptor.ts new file mode 100644 index 000000000000..aa895dad75fe --- /dev/null +++ b/src/Components/Browser.JS/src/Platform/Circuits/ComponentDescriptor.ts @@ -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); + } +} diff --git a/src/Components/Browser.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts b/src/Components/Browser.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts index bc4425ee1bd2..f929ffa75a6a 100644 --- a/src/Components/Browser.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts +++ b/src/Components/Browser.JS/src/Platform/Circuits/DefaultReconnectDisplay.ts @@ -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(';'); diff --git a/src/Components/Browser.JS/src/Platform/Circuits/RenderQueue.ts b/src/Components/Browser.JS/src/Platform/Circuits/RenderQueue.ts new file mode 100644 index 000000000000..be82d8ee71f4 --- /dev/null +++ b/src/Components/Browser.JS/src/Platform/Circuits/RenderQueue.ts @@ -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(); + + 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 { + try { + await connection.send('OnRenderCompleted', batchId, null); + } catch { + this.logger.log(LogLevel.Warning, `Failed to deliver completion notification for render '${batchId}'.`); + } + } +} diff --git a/src/Components/Browser.JS/src/Platform/Logging/ILogger.ts b/src/Components/Browser.JS/src/Platform/Logging/ILogger.ts new file mode 100644 index 000000000000..049dcd936917 --- /dev/null +++ b/src/Components/Browser.JS/src/Platform/Logging/ILogger.ts @@ -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; +} diff --git a/src/Components/Browser.JS/src/Platform/Logging/Loggers.ts b/src/Components/Browser.JS/src/Platform/Logging/Loggers.ts new file mode 100644 index 000000000000..547e78a2e451 --- /dev/null +++ b/src/Components/Browser.JS/src/Platform/Logging/Loggers.ts @@ -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; + } + } + } +} diff --git a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts index 3d2317668f3c..478d9cf38121 100644 --- a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts @@ -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, referenceFrames: ArrayValues) { + public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment, referenceFrames: ArrayValues): 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 = '!'; +} diff --git a/src/Components/Browser.JS/src/Rendering/LogicalElements.ts b/src/Components/Browser.JS/src/Rendering/LogicalElements.ts index 28d153f37dd2..7a41484058ae 100644 --- a/src/Components/Browser.JS/src/Rendering/LogicalElements.ts +++ b/src/Components/Browser.JS/src/Rendering/LogicalElements.ts @@ -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 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 + // + //

Static content

+ // +

Prerendered content

+ +
+ + `); + + 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(` + + + Page + + +
Preamble
+ +

Prerendered content

+ +
+ +

Prerendered content

+ +
+ + `); + + 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(` + + + Page + + +
Preamble
+ +

Prerendered content

+ +
+ +

Prerendered content

+ +
+ + `); + + expect(() => discoverPrerenderedCircuits(dom.window.document)) + .toThrow(); + }); + + it('discoverPrerenderedCircuits initializes circuits', () => { + const dom = new JSDOM(` + + + Page + + +
Preamble
+ +

Prerendered content

+ +
+ +

Prerendered content

+ +
+ + `); + + 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(); + } + } + + }); + +}); diff --git a/src/Components/Browser.JS/tests/RenderQueue.test.ts b/src/Components/Browser.JS/tests/RenderQueue.test.ts new file mode 100644 index 000000000000..653a0b24be26 --- /dev/null +++ b/src/Components/Browser.JS/tests/RenderQueue.test.ts @@ -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); + }); + +}); diff --git a/src/Components/Browser.JS/tests/tsconfig.json b/src/Components/Browser.JS/tests/tsconfig.json index 4eb37fee05c6..48610a17ff35 100644 --- a/src/Components/Browser.JS/tests/tsconfig.json +++ b/src/Components/Browser.JS/tests/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "../tsconfig.base.json" + "extends": "../tsconfig.json", } diff --git a/src/Components/Browser.JS/tsconfig.base.json b/src/Components/Browser.JS/tsconfig.json similarity index 86% rename from src/Components/Browser.JS/tsconfig.base.json rename to src/Components/Browser.JS/tsconfig.json index 3d7c554d329e..c6a7ab5e06f2 100644 --- a/src/Components/Browser.JS/tsconfig.base.json +++ b/src/Components/Browser.JS/tsconfig.json @@ -4,6 +4,7 @@ "noEmitOnError": true, "removeComments": false, "sourceMap": true, + "downlevelIteration": true, "target": "es5", "lib": ["es2015", "dom"], "strict": true diff --git a/src/Components/Browser.JS/yarn.lock b/src/Components/Browser.JS/yarn.lock index 61084684f95c..dfb2e1969aa2 100644 --- a/src/Components/Browser.JS/yarn.lock +++ b/src/Components/Browser.JS/yarn.lock @@ -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" diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 84a51902e126..a7a92c962b97 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -638,13 +638,21 @@ protected LayoutComponentBase() { } } 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 Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer { public HtmlRenderer(System.IServiceProvider serviceProvider, System.Func 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> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; } - public System.Threading.Tasks.Task> RenderComponentAsync(Microsoft.AspNetCore.Components.ParameterCollection initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } + public System.Threading.Tasks.Task RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; } + public System.Threading.Tasks.Task RenderComponentAsync(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 @@ public abstract partial class UriHelperBase : Microsoft.AspNetCore.Components.Se { protected UriHelperBase() { } public event System.EventHandler 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); diff --git a/src/Components/Components/src/Rendering/ComponentRenderedText.cs b/src/Components/Components/src/Rendering/ComponentRenderedText.cs new file mode 100644 index 000000000000..400fda76437a --- /dev/null +++ b/src/Components/Components/src/Rendering/ComponentRenderedText.cs @@ -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 +{ + /// + /// Represents the result of rendering a component into static html. + /// + public readonly struct ComponentRenderedText + { + internal ComponentRenderedText(int componentId, IEnumerable tokens) + { + ComponentId = componentId; + Tokens = tokens; + } + + /// + /// Gets the id associated with the component. + /// + public int ComponentId { get; } + + /// + /// Gets the sequence of tokens that when concatenated represent the html for the rendered component. + /// + public IEnumerable Tokens { get; } + } +} diff --git a/src/Components/Components/src/Rendering/HtmlRenderer.cs b/src/Components/Components/src/Rendering/HtmlRenderer.cs index 97d4de456118..0366d5c1afa9 100644 --- a/src/Components/Components/src/Rendering/HtmlRenderer.cs +++ b/src/Components/Components/src/Rendering/HtmlRenderer.cs @@ -47,21 +47,14 @@ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) /// The type of the . /// A with the initial parameters to render the component. /// A that on completion returns a sequence of fragments that represent the HTML text of the component. - public async Task> RenderComponentAsync(Type componentType, ParameterCollection initialParameters) + public async Task 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(); - } - else - { - var result = new List(); - var newPosition = RenderFrames(result, frames, 0, frames.Count); - Debug.Assert(newPosition == frames.Count); - return result; - } + var result = new List(); + var newPosition = RenderFrames(result, frames, 0, frames.Count); + Debug.Assert(newPosition == frames.Count); + return new ComponentRenderedText(componentId, result); } /// @@ -71,7 +64,7 @@ public async Task> RenderComponentAsync(Type componentType, /// The type of the . /// A with the initial parameters to render the component. /// A that on completion returns a sequence of fragments that represent the HTML text of the component. - public Task> RenderComponentAsync(ParameterCollection initialParameters) where TComponent : IComponent + public Task RenderComponentAsync(ParameterCollection initialParameters) where TComponent : IComponent { return RenderComponentAsync(typeof(TComponent), initialParameters); } @@ -227,14 +220,14 @@ private int RenderAttributes( return position + maxElements; } - private async Task> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters) + private async Task<(int, ArrayRange)> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters) { var component = InstantiateComponent(componentType); var componentId = AssignRootComponentId(component); await RenderRootComponentAsync(componentId, initialParameters); - return GetCurrentRenderTreeFrames(componentId); + return (componentId, GetCurrentRenderTreeFrames(componentId)); } } } diff --git a/src/Components/Components/src/Rendering/Renderer.cs b/src/Components/Components/src/Rendering/Renderer.cs index 22c75e64f7f1..7548920b8562 100644 --- a/src/Components/Components/src/Rendering/Renderer.cs +++ b/src/Components/Components/src/Rendering/Renderer.cs @@ -227,6 +227,10 @@ public Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs) task = callback.InvokeAsync(eventArgs); } + catch (Exception e) + { + HandleException(e); + } finally { _isBatchInProgress = false; @@ -336,7 +340,7 @@ internal void AddToPendingTasks(Task task) // 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 @@ private void ProcessRenderQueue() // 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 @@ private void ProcessRenderQueue() } } - private Task InvokeRenderCompletedCalls(ArrayRange updatedComponents) + private Task InvokeRenderCompletedCalls(ArrayRange 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 batch = null; var array = updatedComponents.Array; for (var i = 0; i < updatedComponents.Count; i++) @@ -470,36 +505,83 @@ private Task InvokeRenderCompletedCalls(ArrayRange updatedCompon 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(); + NotifyRenderCompleted(componentState, ref batch); + } + } - // 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; - } - } + return batch != null ? + Task.WhenAll(batch) : + Task.CompletedTask; - // The Task is incomplete. - // Queue up the task and we can inspect it later. - batch = batch ?? new List(); - batch.Add(GetErrorHandledTask(task)); + } + + 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 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); } } - return batch != null ? + var result = batch != null ? Task.WhenAll(batch) : Task.CompletedTask; + + await result; + } + + private void NotifyRenderCompleted(ComponentState state, ref List 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(); + batch.Add(GetErrorHandledTask(task)); } private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry) diff --git a/src/Components/Components/src/Services/UriHelperBase.cs b/src/Components/Components/src/Services/UriHelperBase.cs index aebf73aad552..4c067404d49d 100644 --- a/src/Components/Components/src/Services/UriHelperBase.cs +++ b/src/Components/Components/src/Services/UriHelperBase.cs @@ -19,7 +19,7 @@ public event EventHandler OnLocationChanged { add { - EnsureInitialized(); + AssertInitialized(); _onLocationChanged += value; } remove @@ -57,7 +57,7 @@ public void NavigateTo(string uri) /// 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. public void NavigateTo(string uri, bool forceLoad) { - EnsureInitialized(); + AssertInitialized(); NavigateToCore(uri, forceLoad); } @@ -70,10 +70,35 @@ public void NavigateTo(string uri, bool forceLoad) protected abstract void NavigateToCore(string uri, bool forceLoad); /// - /// 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. + /// + 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); + } + + /// + /// Allows derived classes to lazyly self initialize. It does nothing unless overriden. /// - protected virtual void InitializeState() + protected virtual void EnsureInitialized() { } @@ -83,7 +108,7 @@ protected virtual void InitializeState() /// The current absolute URI. public string GetAbsoluteUri() { - EnsureInitialized(); + AssertInitialized(); return _uri; } @@ -95,7 +120,7 @@ public string GetAbsoluteUri() /// The URI prefix, which has a trailing slash. public virtual string GetBaseUri() { - EnsureInitialized(); + AssertInitialized(); return _baseUriString; } @@ -107,7 +132,7 @@ public virtual string GetBaseUri() /// The absolute URI. public Uri ToAbsoluteUri(string href) { - EnsureInitialized(); + AssertInitialized(); return new Uri(_baseUri, href); } @@ -185,12 +210,16 @@ protected void TriggerOnLocationChanged() _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."); } } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index b70d6b09f492..c15d1d9bac45 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -2454,6 +2454,50 @@ public void CallsAfterRenderOnEachRender() Assert.Equal(2, component.OnAfterRenderCallCount); } + [Fact] + public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously() + { + // Arrange + var tcs = new TaskCompletionSource(); + var afterRenderTcs = new TaskCompletionSource(); + var onAfterRenderCallCountLog = new List(); + 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(); + var onAfterRenderCallCountLog = new List(); + 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 @@ protected override async Task OnParametersSetAsync() await TaskToAwait; } } + + private class AsyncUpdateTestRenderer : TestRenderer + { + public Func 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(); + } + } } } diff --git a/src/Components/Components/test/Rendering/HtmlRendererTestBase.cs b/src/Components/Components/test/Rendering/HtmlRendererTestBase.cs index 38297e454bd9..8d4ba09a4600 100644 --- a/src/Components/Components/test/Rendering/HtmlRendererTestBase.cs +++ b/src/Components/Components/test/Rendering/HtmlRendererTestBase.cs @@ -390,12 +390,12 @@ public void RenderComponentAsync_ElementRefsNoops() Assert.Equal(expectedHtml, result); } - private IEnumerable GetResult(Task> task) + private IEnumerable GetResult(Task task) { Assert.True(task.IsCompleted); if (task.IsCompletedSuccessfully) { - return task.Result; + return task.Result.Tokens; } else { @@ -440,7 +440,7 @@ public async Task CanRender_AsyncComponent() }))); // Assert - Assert.Equal(expectedHtml, result); + Assert.Equal(expectedHtml, result.Tokens); } [Fact] @@ -465,7 +465,7 @@ public async Task CanRender_NestedAsyncComponents() }))); // Assert - Assert.Equal(expectedHtml, result); + Assert.Equal(expectedHtml, result.Tokens); } diff --git a/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs index 3fe38ad022cd..668439236efa 100644 --- a/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs +++ b/src/Components/Server/src/BlazorPack/BlazorPackHubProtocol.cs @@ -432,6 +432,10 @@ private void SerializeArgument(ref MessagePackWriter writer, object argument) writer.Write(byteArray); break; + case Exception exception: + writer.Write(exception.ToString()); + break; + default: throw new FormatException($"Unsupported argument type {argument.GetType()}"); } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 1ea661622eaa..fe871272d805 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -12,6 +12,22 @@ namespace Microsoft.AspNetCore.Builder /// public static class ComponentEndpointRouteBuilderExtensions { + /// + /// Maps the SignalR to the path and associates + /// the component to this hub instance as the given DOM . + /// + /// The . + /// The . + public static IEndpointConventionBuilder MapComponentHub(this IEndpointRouteBuilder endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + + return endpoints.MapHub(ComponentHub.DefaultPath); + } + /// /// Maps the SignalR to the path and associates /// the component to this hub instance as the given DOM . diff --git a/src/Components/Server/src/Circuits/AutoCancelTaskCompletionSource.cs b/src/Components/Server/src/Circuits/AutoCancelTaskCompletionSource.cs deleted file mode 100644 index 11aae7c6aa81..000000000000 --- a/src/Components/Server/src/Circuits/AutoCancelTaskCompletionSource.cs +++ /dev/null @@ -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 -{ - /// - /// Behaves like a , but automatically times out - /// the underlying task after a given period if not already completed. - /// - internal class AutoCancelTaskCompletionSource - { - private readonly TaskCompletionSource _completionSource; - private readonly CancellationTokenSource _timeoutSource; - - public AutoCancelTaskCompletionSource(int timeoutMilliseconds) - { - _completionSource = new TaskCompletionSource(); - _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 - } - } - } -} diff --git a/src/Components/Server/src/Circuits/CircuitClientProxy.cs b/src/Components/Server/src/Circuits/CircuitClientProxy.cs index bb7d917d4f8c..901c835e4d62 100644 --- a/src/Components/Server/src/Circuits/CircuitClientProxy.cs +++ b/src/Components/Server/src/Circuits/CircuitClientProxy.cs @@ -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; } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index b5801e2ee3d0..ca3497a36a30 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -8,6 +8,7 @@ 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 @@ public CircuitHost( public IDispatcher Dispatcher { get; } - public Task> PrerenderComponentAsync(Type componentType, ParameterCollection parameters) + public Task 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(); + 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 @@ await Renderer.InvokeAsync(async () => // 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 @@ await Renderer.InvokeAsync((Func)(async () => { await OnConnectionDownAsync(CancellationToken.None); await OnCircuitDownAsync(); + Renderer.Dispose(); + _scope.Dispose(); })); - - _scope.Dispose(); - Renderer.Dispose(); } private void AssertInitialized() diff --git a/src/Components/Server/src/Circuits/CircuitPrerenderer.cs b/src/Components/Server/src/Circuits/CircuitPrerenderer.cs index ecec783a9adb..5bb9254a8912 100644 --- a/src/Components/Server/src/Circuits/CircuitPrerenderer.cs +++ b/src/Components/Server/src/Circuits/CircuitPrerenderer.cs @@ -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 static object CircuitHostKey = new object(); + private readonly CircuitFactory _circuitFactory; + private readonly CircuitRegistry _registry; - public CircuitPrerenderer(CircuitFactory circuitFactory) + public CircuitPrerenderer(CircuitFactory circuitFactory, CircuitRegistry registry) { _circuitFactory = circuitFactory; + _registry = registry; } - public async Task> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext) + public async Task PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext) { var context = prerenderingContext.Context; - var circuitHost = _circuitFactory.CreateCircuitHost( - context, - client: CircuitClientProxy.OfflineClient, - GetFullUri(context.Request), - GetFullBaseUri(context.Request)); - - // We don't need to unsubscribe because the circuit host object is scoped to this call. - circuitHost.UnhandledException += CircuitHost_UnhandledException; - - // 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 + var circuitHost = GetOrCreateCircuitHost(context); + + var renderResult = await circuitHost.PrerenderComponentAsync( + prerenderingContext.ComponentType, + prerenderingContext.Parameters); + + circuitHost.Descriptors.Add(new ComponentDescriptor { - return await circuitHost.PrerenderComponentAsync( - prerenderingContext.ComponentType, - prerenderingContext.Parameters); + ComponentType = prerenderingContext.ComponentType, + Prerendered = true + }); + + var result = new[] { + $"", + }.Concat(renderResult.Tokens).Concat( + new[] { + $"" + }); + + 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; } } diff --git a/src/Components/Server/src/Circuits/CircuitRegistry.cs b/src/Components/Server/src/Circuits/CircuitRegistry.cs index ccab938e02a1..ac9c3eef44b7 100644 --- a/src/Components/Server/src/Circuits/CircuitRegistry.cs +++ b/src/Components/Server/src/Circuits/CircuitRegistry.cs @@ -121,6 +121,12 @@ protected virtual bool DisconnectCore(CircuitHost circuitHost, string connection 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 @@ protected virtual bool DisconnectCore(CircuitHost circuitHost, string connection }; DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, entryOptions); - return true; } public virtual async Task ConnectAsync(string circuitId, IClientProxy clientProxy, string connectionId, CancellationToken cancellationToken) diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index 7618e01c1e1c..8dbdf3b5ecdb 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -46,13 +46,16 @@ public override CircuitHost CreateCircuitHost( componentContext.Initialize(client); var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService(); - 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 @@ public override CircuitHost CreateCircuitHost( return circuitHost; } - private static IList ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client) + internal static IList ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client) { - if (client == CircuitClientProxy.OfflineClient) + if (!client.Connected) { - // This is the prerendering case. - return Array.Empty(); + // This is the prerendering case. Descriptors will be registered by the prerenderer. + return new List(); } else { @@ -106,10 +109,6 @@ private static IList ResolveComponentMetadata(HttpContext h } var componentsMetadata = endpoint.Metadata.OfType().ToList(); - if (componentsMetadata.Count == 0) - { - throw new InvalidOperationException("No component was registered with the component hub."); - } return componentsMetadata; } diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 7e40f8800284..2ad9a66afa06 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -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 @@ internal void Initialize(CircuitClientProxy clientProxy) 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); diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 1b0d9d798d18..331733fdce6b 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -5,6 +5,7 @@ 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> _pendingRenders - = new ConcurrentDictionary>(); private readonly ILogger _logger; private long _nextRenderId = 1; + private bool _disposing = false; /// /// Notifies when a rendering exception occured. @@ -54,11 +49,13 @@ public RemoteRenderer( _jsRuntime = jsRuntime; _client = client; - _id = _rendererRegistry.Add(this); + Id = _rendererRegistry.Add(this); _logger = logger; } - internal ConcurrentQueue OfflineRenderBatches = new ConcurrentQueue(); + internal ConcurrentQueue PendingRenderBatches = new ConcurrentQueue(); + + public int Id { get; } /// /// Associates the with the , @@ -73,7 +70,7 @@ public Task AddComponentAsync(Type componentType, string domElementSelector) var attachComponentTask = _jsRuntime.InvokeAsync( "Blazor._internal.attachRootComponentToElement", - _id, + Id, domElementSelector, componentId); CaptureAsyncExceptions(attachComponentTask); @@ -102,13 +99,24 @@ protected override void HandleException(Exception exception) /// 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); } /// protected override Task UpdateDisplayAsync(in RenderBatch batch) { + if (_disposing) + { + // We are being disposed, so do no work. + return Task.FromCanceled(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,79 +134,114 @@ protected override Task UpdateDisplayAsync(in RenderBatch batch) 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; - } + var renderId = Interlocked.Increment(ref _nextRenderId); + + var pendingRender = new PendingRender( + renderId, + batchBytes, + new TaskCompletionSource()); + + // Buffer the rendered batches no matter what. We'll send it down immediately when the client + // is connected or right after the client reconnects. + + 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); - Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId); - return WriteBatchBytes(batchBytes); + return pendingRender.CompletionSource.Task; } - public async Task ProcessBufferedRenderBatches() + public 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); - } + // 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 Task WriteBatchBytes(byte[] batchBytes) + private async Task WriteBatchBytesAsync(PendingRender pending) { - var renderId = Interlocked.Increment(ref _nextRenderId); - - var pendingRenderInfo = new AutoCancelTaskCompletionSource(TimeoutMilliseconds); - _pendingRenders[renderId] = pendingRenderInfo; - // Send the render batch to the client - // If the "send" operation fails (synchronously or asynchronously), abort - // the whole render with that exception + // 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 { - _client.SendAsync("JS.RenderBatch", _id, renderId, batchBytes).ContinueWith(sendTask => + if (!_client.Connected) { - if (sendTask.IsFaulted) - { - pendingRenderInfo.TrySetException(sendTask.Exception); - } - }); + // 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 syncException) + catch (Exception e) { - pendingRenderInfo.TrySetException(syncException); + Log.SendBatchDataFailed(_logger, e); } - // When the render is completed (success, fail, or timeout), stop tracking it - return pendingRenderInfo.Task.ContinueWith(task => + // 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) { - _pendingRenders.TryRemove(renderId, out var ignored); - if (task.IsFaulted) - { - UnhandledException?.Invoke(this, task.Exception); - } - }); + // 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); + } } - public void OnRenderCompleted(long renderId, string errorMessageOrNull) + private void CompleteRender(TaskCompletionSource pendingRenderInfo, string errorMessageOrNull) { - if (_pendingRenders.TryGetValue(renderId, out var pendingRenderInfo)) + if (errorMessageOrNull == null) { - if (errorMessageOrNull == null) - { - pendingRenderInfo.TrySetResult(null); - } - else - { - pendingRenderInfo.TrySetException( - new RemoteRendererException(errorMessageOrNull)); - } + pendingRenderInfo.TrySetResult(null); + } + else + { + pendingRenderInfo.TrySetException(new RemoteRendererException(errorMessageOrNull)); } } + internal readonly struct PendingRender + { + public PendingRender(long batchId, byte[] data, TaskCompletionSource completionSource) + { + BatchId = batchId; + Data = data; + CompletionSource = completionSource; + } + + public long BatchId { get; } + public byte[] Data { get; } + public TaskCompletionSource CompletionSource { get; } + } + private void CaptureAsyncExceptions(Task task) { task.ContinueWith(t => @@ -215,12 +258,14 @@ private static class Log private static readonly Action _unhandledExceptionRenderingComponent; private static readonly Action _beginUpdateDisplayAsync; private static readonly Action _bufferingRenderDisconnectedClient; + private static readonly Action _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 @@ static Log() LogLevel.Trace, EventIds.SkipUpdateDisplayAsync, "Buffering remote render because the client on connection {ConnectionId} is disconnected."); + + _sendBatchDataFailed = LoggerMessage.Define( + 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) diff --git a/src/Components/Server/src/Circuits/RemoteUriHelper.cs b/src/Components/Server/src/Circuits/RemoteUriHelper.cs index 34eaa810db69..2cf1d428314a 100644 --- a/src/Components/Server/src/Circuits/RemoteUriHelper.cs +++ b/src/Components/Server/src/Circuits/RemoteUriHelper.cs @@ -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 _logger; + + public RemoteUriHelper(ILogger logger) + { + _logger = logger; + } + + public bool HasAttachedJSRuntime => _jsRuntime != null; /// /// Initializes the . @@ -22,10 +31,9 @@ public class RemoteUriHelper : UriHelperBase /// The absolute URI of the current page. /// The absolute base URI of the current page. /// The to use for interoperability. - 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 @@ public void Initialize(string uriAbsolute, string baseUriAbsolute) /// The absolute URI of the current page. /// The absolute base URI of the current page. /// The to use for interoperability. - 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( Interop.EnableNavigationInterception, typeof(RemoteUriHelper).Assembly.GetName().Name, nameof(NotifyLocationChanged)); - } + _logger.LogInformation($"{nameof(RemoteUriHelper)} initialized."); + } /// /// For framework use only. @@ -69,14 +74,21 @@ public static void NotifyLocationChanged(string uriAbsolute) var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService(); 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(Interop.NavigateTo, uri, forceLoad); } diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index 76fb91374def..4c6d6d1a5e55 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -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 @@ public override Task OnDisconnectedAsync(Exception exception) 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(); + 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 @@ public async Task ConnectCircuit(string circuitId) { 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 @@ public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string m /// public void OnRenderCompleted(long renderId, string errorMessageOrNull) { + _logger.LogInformation($"Received confirmation for batch {renderId}."); EnsureCircuitHost().Renderer.OnRenderCompleted(renderId, errorMessageOrNull); } diff --git a/src/Components/Server/src/DependencyInjection/ComponentDescriptor.cs b/src/Components/Server/src/DependencyInjection/ComponentDescriptor.cs index 30c63199742f..c97debe7dbdf 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentDescriptor.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentDescriptor.cs @@ -11,10 +11,13 @@ internal class ComponentDescriptor 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; } } } diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index 2a46cccf277d..00697597dcbc 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Components/Server/src/Prerendering/ComponentPrerenderResult.cs b/src/Components/Server/src/Prerendering/ComponentPrerenderResult.cs new file mode 100644 index 000000000000..3a8aa936327e --- /dev/null +++ b/src/Components/Server/src/Prerendering/ComponentPrerenderResult.cs @@ -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 +{ + /// + /// Represents the result of a prerendering an . + /// + public sealed class ComponentPrerenderResult + { + private readonly IEnumerable _result; + + internal ComponentPrerenderResult(IEnumerable result) + { + _result = result; + } + + /// + /// Writes the prerendering result to the given . + /// + /// The the results will be written to. + public void WriteTo(TextWriter writer) + { + foreach (var element in _result) + { + writer.Write(element); + } + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/ComponentPrerrenderingContext.cs b/src/Components/Server/src/Prerendering/ComponentPrerenderingContext.cs similarity index 93% rename from src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/ComponentPrerrenderingContext.cs rename to src/Components/Server/src/Prerendering/ComponentPrerenderingContext.cs index 26bb6326bd55..436c27b31a64 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/ComponentPrerrenderingContext.cs +++ b/src/Components/Server/src/Prerendering/ComponentPrerenderingContext.cs @@ -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; diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/IComponentPrerenderer.cs b/src/Components/Server/src/Prerendering/IComponentPrerenderer.cs similarity index 85% rename from src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/IComponentPrerenderer.cs rename to src/Components/Server/src/Prerendering/IComponentPrerenderer.cs index 24480d0aa030..a8513af17d58 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/Prerendering/IComponentPrerenderer.cs +++ b/src/Components/Server/src/Prerendering/IComponentPrerenderer.cs @@ -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 @@ public interface IComponentPrerenderer /// /// The context in which the prerrendering is happening. /// that will complete when the prerendering is done. - Task> PrerenderComponentAsync(ComponentPrerenderingContext context); + Task PrerenderComponentAsync(ComponentPrerenderingContext context); } } diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs index 559399d9e90e..507ff4e315fe 100644 --- a/src/Components/Server/test/Circuits/CircuitHostTest.cs +++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs @@ -39,6 +39,36 @@ public async Task DisposeAsync_DisposesResources() Assert.True(remoteRenderer.Disposed); } + [Fact] + public async Task DisposeAsync_DisposesRendererWithinSynchronizationContext() + { + // Arrange + var serviceScope = new Mock(); + 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 @@ protected override void Dispose(bool disposing) 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); + } + } } } diff --git a/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs b/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs index cf6c0c39293f..a18a0a010b6b 100644 --- a/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs +++ b/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs @@ -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( + $"(?.*)", + 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 @@ public async Task ExtractsUriFromHttpContext_EmptyPathBase() { // Arrange var circuitFactory = new TestCircuitFactory(); - var circuitPrerenderer = new CircuitPrerenderer(circuitFactory); - var httpContext = new Mock(); - var httpRequest = new Mock().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>()); + 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 @@ public async Task ExtractsUriFromHttpContext_NonemptyPathBase() { // Arrange var circuitFactory = new TestCircuitFactory(); - var circuitPrerenderer = new CircuitPrerenderer(circuitFactory); - var httpContext = new Mock(); - var httpRequest = new Mock().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>()); + 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 @@ public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitCl var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped(_ => { - var uriHelper = new RemoteUriHelper(); - uriHelper.Initialize(uriAbsolute, baseUriAbsolute); + var uriHelper = new RemoteUriHelper(NullLogger.Instance); + uriHelper.InitializeState(uriAbsolute, baseUriAbsolute); return uriHelper; }); var serviceScope = serviceCollection.BuildServiceProvider().CreateScope(); diff --git a/src/Components/Server/test/Circuits/RemoteRendererTest.cs b/src/Components/Server/test/Circuits/RemoteRendererTest.cs index 866cf475bdcc..428e173a2602 100644 --- a/src/Components/Server/test/Circuits/RemoteRendererTest.cs +++ b/src/Components/Server/test/Circuits/RemoteRendererTest.cs @@ -21,7 +21,7 @@ public class RemoteRendererTest : HtmlRendererTestBase { protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider) { - return GetRemoteRenderer(serviceProvider, CircuitClientProxy.OfflineClient); + return GetRemoteRenderer(serviceProvider, new CircuitClientProxy()); } [Fact] @@ -43,7 +43,7 @@ public void WritesAreBufferedWhenTheClientIsOffline() component.TriggerRender(); // Assert - Assert.Equal(2, renderer.OfflineRenderBatches.Count); + Assert.Equal(2, renderer.PendingRenderBatches.Count); } [Fact] @@ -51,15 +51,16 @@ public async Task ProcessBufferedRenderBatches_WritesRenders() { // Arrange var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var renderIds = new List(); + var renderIds = new List(); + + var firstBatchTCS = new TaskCompletionSource(); + var secondBatchTCS = new TaskCompletionSource(); + var thirdBatchTCS = new TaskCompletionSource(); var initialClient = new Mock(); initialClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .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 @@ public async Task ProcessBufferedRenderBatches_WritesRenders() builder.AddContent(1, "some text"); builder.CloseElement(); }); - var client = new Mock(); client.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .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((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 @@ public async Task ProcessBufferedRenderBatches_WritesRenders() // 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); } + + secondBatchTCS.SetResult(null); + thirdBatchTCS.SetResult(null); + + // Assert + 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(); + var secondBatchTCS = new TaskCompletionSource(); + var offlineClient = new CircuitClientProxy(new Mock(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(); + var onlineClient = new Mock(); + onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) + .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); + + // This produces the initial batch (id = 2) + var result = await renderer.RenderComponentAsync( + ParameterCollection.FromDictionary(new Dictionary + { + [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(); + 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(); + var secondBatchTCS = new TaskCompletionSource(); + var offlineClient = new CircuitClientProxy(new Mock(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(); + var onlineClient = new Mock(); + onlineClient.Setup(c => c.SendCoreAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1])) + .Returns((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task); + + // This produces the initial batch (id = 2) + var result = await renderer.RenderComponentAsync( + ParameterCollection.FromDictionary(new Dictionary + { + [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(); + 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 - client.Verify(c => c.SendCoreAsync("JS.RenderBatch", It.IsAny(), It.IsAny()), Times.Exactly(2)); + 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(ParameterCollection.Empty); + var second = await renderer.RenderComponentAsync(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(); + jsRuntime.Setup(r => r.InvokeAsync( + "Blazor._internal.attachRootComponentToElement", + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(Task.FromResult(null)); + return new RemoteRenderer( serviceProvider, new RendererRegistry(), - Mock.Of(), + jsRuntime.Object, circuitClientProxy, Dispatcher, HtmlEncoder.Default, @@ -113,7 +264,16 @@ private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, Circu 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 @@ public void TriggerRender() 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(nameof(Content)); + Trigger ??= parameters.GetValueOrDefault(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(); + } + } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerSideAppTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerSideAppTest.cs index f83c72f2e5c4..685e0838e5e8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerSideAppTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerSideAppTest.cs @@ -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 @@ public ServerSideAppTest( _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 @@ public void RendersContinueAfterReconnect() _ => 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."))); + } + } } diff --git a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs index 4e7461c7fa7c..5c8e194cac28 100644 --- a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs +++ b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs @@ -459,8 +459,7 @@ public void CanCaptureReferencesToDynamicallyAddedComponents() public void CanUseJsInteropForRefElementsDuringOnAfterRender() { var appElement = MountTestComponent(); - 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] diff --git a/src/Components/test/testassets/ComponentsApp.App/App.razor b/src/Components/test/testassets/ComponentsApp.App/App.razor index 033dcc04b802..03ebda9b0f05 100644 --- a/src/Components/test/testassets/ComponentsApp.App/App.razor +++ b/src/Components/test/testassets/ComponentsApp.App/App.razor @@ -1,5 +1,12 @@ +@using Microsoft.AspNetCore.Components; - + + + + +@functions{ + [Parameter] public string Name { get; set; } +} diff --git a/src/Components/test/testassets/ComponentsApp.App/Pages/Error.razor b/src/Components/test/testassets/ComponentsApp.App/Pages/Error.razor new file mode 100644 index 000000000000..35f83bf20da4 --- /dev/null +++ b/src/Components/test/testassets/ComponentsApp.App/Pages/Error.razor @@ -0,0 +1,19 @@ +@page "/error" +@using Microsoft.AspNetCore.Components +

Error

+@if(ShouldCauseError) +{ + throw new InvalidOperationException("Exception while rendering"); +} +else +{ + +} +@functions { + public bool ShouldCauseError { get; set; } + + void CauseError() + { + ShouldCauseError = true; + } +} diff --git a/src/Components/test/testassets/ComponentsApp.App/Pages/Greeter.razor b/src/Components/test/testassets/ComponentsApp.App/Pages/Greeter.razor new file mode 100644 index 000000000000..cb7f5bc1924c --- /dev/null +++ b/src/Components/test/testassets/ComponentsApp.App/Pages/Greeter.razor @@ -0,0 +1,8 @@ +@page "/greeter" +@using Microsoft.AspNetCore.Components +

Greeter

+

Hello @Name

+ +@functions { + [CascadingParameter(Name=nameof(Name))] public string Name { get; set; } +} diff --git a/src/Components/test/testassets/ComponentsApp.App/Shared/NavMenu.razor b/src/Components/test/testassets/ComponentsApp.App/Shared/NavMenu.razor index eecc42248e7e..d68c69b691ee 100644 --- a/src/Components/test/testassets/ComponentsApp.App/Shared/NavMenu.razor +++ b/src/Components/test/testassets/ComponentsApp.App/Shared/NavMenu.razor @@ -27,6 +27,16 @@ Ticker + + diff --git a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj index 9822285d5261..2c1bf87bb73d 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj +++ b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -7,6 +7,7 @@ + diff --git a/src/Components/test/testassets/ComponentsApp.Server/Pages/Index.cshtml b/src/Components/test/testassets/ComponentsApp.Server/Pages/Index.cshtml index 9988e733aedd..ff0648e9db45 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Pages/Index.cshtml +++ b/src/Components/test/testassets/ComponentsApp.Server/Pages/Index.cshtml @@ -1,4 +1,4 @@ -@page "{*clientroutes}" +@page @using ComponentsApp.App @@ -7,13 +7,15 @@ Razor Components - + - @(await Html.RenderComponentAsync()) + + @(await Html.RenderComponentAsync(new { Name="Guest" })) + diff --git a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs index cb855dbdf2fc..b7eea9fb37b0 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/Startup.cs +++ b/src/Components/test/testassets/ComponentsApp.Server/Startup.cs @@ -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 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); - endpoints.MapComponentHub("app"); + endpoints.MapControllers(); + endpoints.MapComponentHub(); + endpoints.MapFallbackToPage("/Index"); }); } } diff --git a/src/Components/test/testassets/ComponentsApp.Server/appsettings.Development.json b/src/Components/test/testassets/ComponentsApp.Server/appsettings.Development.json new file mode 100644 index 000000000000..9f98fa46565a --- /dev/null +++ b/src/Components/test/testassets/ComponentsApp.Server/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug" + }, + "Console": { + "LogLevel": { + "Default": "Debug" + } + } + } +} diff --git a/src/Components/test/testassets/ComponentsApp.Server/appsettings.json b/src/Components/test/testassets/ComponentsApp.Server/appsettings.json new file mode 100644 index 000000000000..26bb0ac7ac67 --- /dev/null +++ b/src/Components/test/testassets/ComponentsApp.Server/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/Components/test/testassets/ComponentsApp.Server/wwwroot/css/site.css b/src/Components/test/testassets/ComponentsApp.Server/wwwroot/css/site.css index fa2c7f19b7d6..2789bc7b8da8 100644 --- a/src/Components/test/testassets/ComponentsApp.Server/wwwroot/css/site.css +++ b/src/Components/test/testassets/ComponentsApp.Server/wwwroot/css/site.css @@ -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; diff --git a/src/Components/test/testassets/ComponentsApp.Server/wwwroot/favicon.ico b/src/Components/test/testassets/ComponentsApp.Server/wwwroot/favicon.ico new file mode 100644 index 000000000000..a3a799985c43 Binary files /dev/null and b/src/Components/test/testassets/ComponentsApp.Server/wwwroot/favicon.ico differ diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml index 628265b26bca..dd5ee32cbcc5 100644 --- a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml @@ -1,5 +1,6 @@ @page @using BasicTestApp.RouterTest +@using Microsoft.AspNetCore.Mvc.ViewFeatures diff --git a/src/Components/test/testassets/TestServer/TestServer.csproj b/src/Components/test/testassets/TestServer/TestServer.csproj index 281e0fbca387..726fdcb38cb8 100644 --- a/src/Components/test/testassets/TestServer/TestServer.csproj +++ b/src/Components/test/testassets/TestServer/TestServer.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Components/test/testassets/TestServer/appsettings.Development.json b/src/Components/test/testassets/TestServer/appsettings.Development.json index fa8ce71a97a3..35f148203c72 100644 --- a/src/Components/test/testassets/TestServer/appsettings.Development.json +++ b/src/Components/test/testassets/TestServer/appsettings.Development.json @@ -1,10 +1,10 @@ -{ +{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", - "System": "Information", - "Microsoft": "Information" + "System": "Debug", + "Microsoft": "Debug" } } } diff --git a/src/Middleware/CORS/test/FunctionalTests/package.json b/src/Middleware/CORS/test/FunctionalTests/package.json index a2ec7219398e..e5ab47f4c9ce 100644 --- a/src/Middleware/CORS/test/FunctionalTests/package.json +++ b/src/Middleware/CORS/test/FunctionalTests/package.json @@ -3,7 +3,7 @@ "devDependencies": { "jest": "^23.6.0", "merge": "^1.2.1", - "puppeteer": "^1.13.0" + "puppeteer": "^1.14.0" }, "dependencies": {}, "scripts": { diff --git a/src/Middleware/CORS/test/FunctionalTests/yarn.lock b/src/Middleware/CORS/test/FunctionalTests/yarn.lock index e9b38ac65c34..aece67a3460a 100644 --- a/src/Middleware/CORS/test/FunctionalTests/yarn.lock +++ b/src/Middleware/CORS/test/FunctionalTests/yarn.lock @@ -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" diff --git a/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj b/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj new file mode 100644 index 000000000000..d87791578898 --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.netcoreapp3.0.cs b/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.netcoreapp3.0.cs new file mode 100644 index 000000000000..11c87ec6eecb --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/ref/Microsoft.AspNetCore.Mvc.Components.Prerendering.netcoreapp3.0.cs @@ -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 RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } + } +} diff --git a/src/Mvc/Mvc.Components.Prerendering/src/HtmlContentPrerenderComponentResultAdapter.cs b/src/Mvc/Mvc.Components.Prerendering/src/HtmlContentPrerenderComponentResultAdapter.cs new file mode 100644 index 000000000000..5eeabd70508e --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/src/HtmlContentPrerenderComponentResultAdapter.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Mvc/Mvc.Components.Prerendering/src/HtmlHelperComponentPrerenderingExtensions.cs b/src/Mvc/Mvc.Components.Prerendering/src/HtmlHelperComponentPrerenderingExtensions.cs new file mode 100644 index 000000000000..e21673f18c59 --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/src/HtmlHelperComponentPrerenderingExtensions.cs @@ -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 +{ + /// + /// Extensions for rendering components. + /// + public static class HtmlHelperComponentPrerenderingExtensions + { + /// + /// Renders the . + /// + /// The . + /// The HTML produced by the rendered . + public static Task RenderComponentAsync(this IHtmlHelper htmlHelper) where TComponent : IComponent + { + if (htmlHelper == null) + { + throw new ArgumentNullException(nameof(htmlHelper)); + } + + return htmlHelper.RenderComponentAsync(null); + } + + /// + /// Renders the . + /// + /// The . + /// An containing the parameters to pass + /// to the component. + /// The HTML produced by the rendered . + public static async Task RenderComponentAsync( + 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(); + + 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); + } + } +} \ No newline at end of file diff --git a/src/Mvc/Mvc.Components.Prerendering/src/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj b/src/Mvc/Mvc.Components.Prerendering/src/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj new file mode 100644 index 000000000000..838dd026dccd --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/src/Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core MVC component interactive rendering features. Contains types to integrate server-side rendered components into MVC Views and Pages. + + netcoreapp3.0 + $(NoWarn);CS1591 + true + aspnetcore;aspnetcoremvc + false + true + + + + + + + + diff --git a/src/Mvc/Mvc.Components.Prerendering/test/HtmlHelperComponentPrerenderingExtensionsTests.cs b/src/Mvc/Mvc.Components.Prerendering/test/HtmlHelperComponentPrerenderingExtensionsTests.cs new file mode 100644 index 000000000000..942e9b7e1927 --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/test/HtmlHelperComponentPrerenderingExtensionsTests.cs @@ -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( + $"(?.*)", + 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(() => helper.RenderComponentAsync()); + + // Assert + Assert.Equal(expectedmessage, exception.Message); + } + + [Fact] + public async Task CanRender_ParameterlessComponent() + { + // Arrange + var helper = CreateHelper(); + + // Act + var result = await helper.RenderComponentAsync(); + var unwrappedContent = GetUnwrappedContent(result); + + // Assert + Assert.Equal("

Hello world!

", unwrappedContent); + } + + [Fact] + public async Task CanRender_ComponentWithParametersObject() + { + // Arrange + var helper = CreateHelper(); + + // Act + var result = await helper.RenderComponentAsync(new + { + Name = "Guest" + }); + + var unwrappedContent = GetUnwrappedContent(result); + + // Assert + Assert.Equal("

Hello Guest!

", unwrappedContent); + } + + [Fact] + public async Task CanCatch_ComponentWithSynchronousException() + { + // Arrange + var helper = CreateHelper(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(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(() => helper.RenderComponentAsync(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(() => helper.RenderComponentAsync(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(() => helper.RenderComponentAsync(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 = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DateSummaryFC
06/05/2018Freezing3333
07/05/2018Bracing5757
08/05/2018Freezing99
09/05/2018Balmy44
10/05/2018Chilly2929
"; + + // Act + var result = await helper.RenderComponentAsync(); + 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("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 configureServices = null) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(HtmlEncoder.Default); + configureServices = configureServices ?? (s => s.AddRazorComponents()); + configureServices?.Invoke(services); + + var helper = new Mock(); + 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; + } + } +} \ No newline at end of file diff --git a/src/Mvc/Mvc.Components.Prerendering/test/Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj b/src/Mvc/Mvc.Components.Prerendering/test/Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj new file mode 100644 index 000000000000..5cfbdb8ff4bc --- /dev/null +++ b/src/Mvc/Mvc.Components.Prerendering/test/Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs index 6572188d79fd..74d105b6c73e 100644 --- a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs +++ b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs @@ -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> 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 @@ public HtmlHelperOptions() { } } public static partial class HtmlHelperRazorComponentExtensions { - public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } + public static System.Threading.Tasks.Task RenderStaticComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } [System.Diagnostics.DebuggerStepThroughAttribute] - public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } + public static System.Threading.Tasks.Task RenderStaticComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } } public partial class HtmlHelper : Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper { diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index faeeebb79d4c..cb4cc88e0817 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -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 @@ internal static void AddViewServices(IServiceCollection services) // // Component prerendering // - services.TryAddSingleton(); + services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperRazorComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperRazorComponentExtensions.cs index 50f3fc58aced..1ce840b80c49 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperRazorComponentExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperRazorComponentExtensions.cs @@ -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 @@ public static class HtmlHelperRazorComponentExtensions /// /// The . /// The HTML produced by the rendered . - public static Task RenderComponentAsync(this IHtmlHelper htmlHelper) where TComponent : IComponent + public static Task RenderStaticComponentAsync(this IHtmlHelper htmlHelper) where TComponent : IComponent { if (htmlHelper == null) { throw new ArgumentNullException(nameof(htmlHelper)); } - return htmlHelper.RenderComponentAsync(null); + return htmlHelper.RenderStaticComponentAsync(null); } /// @@ -39,7 +38,7 @@ public static Task RenderComponentAsync(this IHtmlHelp /// An containing the parameters to pass /// to the component. /// The HTML produced by the rendered . - public static async Task RenderComponentAsync( + public static async Task RenderStaticComponentAsync( this IHtmlHelper htmlHelper, object parameters) where TComponent : IComponent { @@ -50,14 +49,16 @@ public static async Task RenderComponentAsync( var httpContext = htmlHelper.ViewContext.HttpContext; var serviceProvider = httpContext.RequestServices; - var prerenderer = serviceProvider.GetRequiredService(); + var prerenderer = serviceProvider.GetRequiredService(); - 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); } diff --git a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/HttpUriHelper.cs b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/HttpUriHelper.cs index 15cef58c04e1..01bc9c12f30f 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/HttpUriHelper.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Infrastructure/HttpUriHelper.cs @@ -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."); } } } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/MvcRazorComponentPrerenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/MvcRazorComponentPrerenderer.cs deleted file mode 100644 index 8f39ffa2ab62..000000000000 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/MvcRazorComponentPrerenderer.cs +++ /dev/null @@ -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> 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(); - helper.InitializeState(context.Context); - using (var htmlRenderer = new HtmlRenderer(context.Context.RequestServices, _encoder.Encode, dispatcher)) - { - return await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync( - context.ComponentType, - parameters)); - } - } - } -} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs new file mode 100644 index 000000000000..171a8e437302 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs @@ -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> 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(); + 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 += "/"; + } + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs index 014c5c200157..8570892ba114 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs @@ -7,7 +7,6 @@ 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 @@ public async Task CanRender_ParameterlessComponent() var writer = new StringWriter(); // Act - var result = await helper.RenderComponentAsync(); + var result = await helper.RenderStaticComponentAsync(); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); @@ -45,7 +44,7 @@ public async Task CanRender_ComponentWithParametersObject() var writer = new StringWriter(); // Act - var result = await helper.RenderComponentAsync(new + var result = await helper.RenderStaticComponentAsync(new { Name = "Steve" }); @@ -63,7 +62,7 @@ public async Task CanCatch_ComponentWithSynchronousException() var helper = CreateHelper(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new + var exception = await Assert.ThrowsAsync(() => helper.RenderStaticComponentAsync(new { IsAsync = false })); @@ -79,7 +78,7 @@ public async Task CanCatch_ComponentWithAsynchronousException() var helper = CreateHelper(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new + var exception = await Assert.ThrowsAsync(() => helper.RenderStaticComponentAsync(new { IsAsync = true })); @@ -95,7 +94,7 @@ public async Task Rendering_ComponentWithJsInteropThrows() var helper = CreateHelper(); // Act & Assert - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new + var exception = await Assert.ThrowsAsync(() => helper.RenderStaticComponentAsync(new { JsInterop = true })); @@ -122,16 +121,17 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException() var writer = new StringWriter(); // Act - var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(new + var exception = await Assert.ThrowsAsync(() => helper.RenderStaticComponentAsync(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 @@ public async Task CanRender_AsyncComponent() "; // Act - var result = await helper.RenderComponentAsync(); + var result = await helper.RenderStaticComponentAsync(); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); @@ -196,7 +196,7 @@ private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); configureServices?.Invoke(services); diff --git a/src/Mvc/Mvc.sln b/src/Mvc/Mvc.sln index d345640b8810..001069501bdf 100644 --- a/src/Mvc/Mvc.sln +++ b/src/Mvc/Mvc.sln @@ -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} diff --git a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj index 6fe7c2591d8b..a20fbcab06a1 100644 --- a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj +++ b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Mvc/shared/Mvc.Views.TestCommon/Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj b/src/Mvc/shared/Mvc.Views.TestCommon/Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj index 7396524b36c3..e91f1684ff64 100644 --- a/src/Mvc/shared/Mvc.Views.TestCommon/Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj +++ b/src/Mvc/shared/Mvc.Views.TestCommon/Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Mvc/test/Mvc.FunctionalTests/BasicTests.cs b/src/Mvc/test/Mvc.FunctionalTests/BasicTests.cs index 9e63a4baaaa1..eb8dde518a6c 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/BasicTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/BasicTests.cs @@ -478,6 +478,16 @@ public async Task ApplicationAssemblyPartIsListedAsFirstAssembly() // Act var response = await Client.GetStringAsync("Home/GetAssemblyPartData"); var assemblyParts = JsonConvert.DeserializeObject>(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 // diff --git a/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs b/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs index 53f7f2422121..a7358e95a308 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/ComponentRenderingFunctionalTests.cs @@ -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> { + private static readonly Regex ContentWrapperRegex = new Regex( + $"(?.*)", + RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line + public ComponentRenderingFunctionalTests(MvcTestFixture fixture) { Factory = fixture; @@ -35,14 +40,15 @@ public async Task Renders_BasicComponent() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - AssertComponent("\n

Hello world!

\n", "Greetings", content); + AssertComponent("\n

Hello world!

", "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 @@ public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - AssertComponent("\n

Hello world!

\n", "Greetings", content); + AssertComponent("\n

Hello world!

", "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 @@ public async Task Renders_RoutingComponent() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - AssertComponent("\n Router component\n

Routed successfully

\n", "Routing", content); + AssertComponent("\nRouter component\n

Routed successfully

", "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 @@ public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var content = await response.Content.ReadAsStringAsync(); - AssertComponent("\n Router component\n

Routed successfully

\n", "Routing", content); + AssertComponent("\nRouter component\n

Routed successfully

", "Routing", content); + } + + [Fact] + public async Task Renders_BasicComponentInteractive_UsingRazorComponents_Prerenderer() + { + // Arrange & Act + 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("

Hello world!

", "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

Routed successfully

", "Routing", content, unwrap: true); } [Fact] - public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer() + public async Task Renders_ThrowingComponent_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/throws"); @@ -103,7 +142,7 @@ public async Task Renders_AsyncComponent() { // Arrange & Act var expectedHtml = @" -

Weather forecast

+

Weather forecast

This component demonstrates fetching data from the server.

@@ -150,7 +189,6 @@ public async Task Renders_AsyncComponent() - "; var client = CreateClient(Factory); var response = await client.GetAsync("http://localhost/components"); @@ -162,14 +200,22 @@ public async Task Renders_AsyncComponent() 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 @@ private class LoopHttpHandler : DelegatingHandler { } - private HttpClient CreateClient(MvcTestFixture fixture, Action configure = null) + private HttpClient CreateClient(WebApplicationFactory 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 diff --git a/src/Mvc/test/WebSites/BasicWebSite/BasicWebSite.csproj b/src/Mvc/test/WebSites/BasicWebSite/BasicWebSite.csproj index 12f5d54167d7..800d0343925a 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/BasicWebSite.csproj +++ b/src/Mvc/test/WebSites/BasicWebSite/BasicWebSite.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Mvc/test/WebSites/BasicWebSite/Controllers/RazorComponentsController.cs b/src/Mvc/test/WebSites/BasicWebSite/Controllers/RazorComponentsController.cs index 28aab986a3e1..2234bf7ba63b 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/Controllers/RazorComponentsController.cs +++ b/src/Mvc/test/WebSites/BasicWebSite/Controllers/RazorComponentsController.cs @@ -5,6 +5,7 @@ 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 @@ public class RazorComponentsController : Controller } }; - [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")] diff --git a/src/Mvc/test/WebSites/BasicWebSite/Views/RazorComponents/Index.cshtml b/src/Mvc/test/WebSites/BasicWebSite/Views/RazorComponents/Index.cshtml index 8a0211c49816..bdb63e5b808a 100644 --- a/src/Mvc/test/WebSites/BasicWebSite/Views/RazorComponents/Index.cshtml +++ b/src/Mvc/test/WebSites/BasicWebSite/Views/RazorComponents/Index.cshtml @@ -1,13 +1,35 @@ @using BasicWebSite.RazorComponents; +@model bool;

Razor components

- @(await Html.RenderComponentAsync()) + @if (Model) + { + @(await Html.RenderStaticComponentAsync()) + } + else + { + @(await Html.RenderComponentAsync()) + }
- @(await Html.RenderComponentAsync(new { StartDate = new DateTime(2019, 01, 15) })) + @if (Model) + { + @(await Html.RenderStaticComponentAsync(new { StartDate = new DateTime(2019, 01, 15) })) + } + else + { + @(await Html.RenderComponentAsync(new { StartDate = new DateTime(2019, 01, 15) })) + }
- @(await Html.RenderComponentAsync()) + @if (Model) + { + @(await Html.RenderStaticComponentAsync()); + } + else + { + @(await Html.RenderComponentAsync()); + }
\ No newline at end of file diff --git a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj index 436c21eb41f3..1ca1d65b8871 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj +++ b/src/ProjectTemplates/Web.ProjectTemplates/Microsoft.DotNet.Web.ProjectTemplates.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -31,7 +31,7 @@ - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/RazorComponentsWeb-CSharp.csproj.in b/src/ProjectTemplates/Web.ProjectTemplates/RazorComponentsWeb-CSharp.csproj.in index e1d4e827c419..b32e272e0c8d 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/RazorComponentsWeb-CSharp.csproj.in +++ b/src/ProjectTemplates/Web.ProjectTemplates/RazorComponentsWeb-CSharp.csproj.in @@ -9,7 +9,7 @@ - + diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/Host.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/Host.cshtml index 20d21a9a42d1..a181c7109a77 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/Host.cshtml +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Pages/Host.cshtml @@ -1,4 +1,4 @@ -@page "/{*clientPath}" +@page "/" @namespace RazorComponentsWeb_CSharp.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs index 9bf1044b02f8..4e418d70a415 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs @@ -53,7 +53,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); - endpoints.MapComponentHub("app"); + endpoints.MapComponentHub(); + endpoints.MapFallbackToPage("/Host"); }); } } diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/wwwroot/css/site.css b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/wwwroot/css/site.css index fa2c7f19b7d6..2789bc7b8da8 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/wwwroot/css/site.css +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/wwwroot/css/site.css @@ -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; diff --git a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs index b00ec602beb5..e7b1ea50e8a2 100644 --- a/src/ProjectTemplates/test/Helpers/AspNetProcess.cs +++ b/src/ProjectTemplates/test/Helpers/AspNetProcess.cs @@ -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 @@ namespace Templates.Test.Helpers { + [DebuggerDisplay("{ToString(),nq}")] public class AspNetProcess : IDisposable { private const string ListeningMessagePrefix = "Now listening on: "; @@ -142,5 +144,24 @@ public void Dispose() _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; + } } } diff --git a/src/ProjectTemplates/test/Helpers/ProcessEx.cs b/src/ProjectTemplates/test/Helpers/ProcessEx.cs index 11e34f9972b1..af16ef384966 100644 --- a/src/ProjectTemplates/test/Helpers/ProcessEx.cs +++ b/src/ProjectTemplates/test/Helpers/ProcessEx.cs @@ -75,6 +75,8 @@ public string Output public int ExitCode => _process.ExitCode; + public object Id => _process.Id; + public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary envVars = null) { var startInfo = new ProcessStartInfo(command, args) diff --git a/src/ProjectTemplates/test/Helpers/Project.cs b/src/ProjectTemplates/test/Helpers/Project.cs index 16b9bd9a8f92..4fa536b2cc21 100644 --- a/src/ProjectTemplates/test/Helpers/Project.cs +++ b/src/ProjectTemplates/test/Helpers/Project.cs @@ -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 @@ namespace Templates.Test.Helpers { + [DebuggerDisplay("{ToString(),nq}")] public class Project { public const string DefaultFramework = "netcoreapp3.0"; @@ -384,5 +386,7 @@ public void Release() } } } + + public override string ToString() => $"{ProjectName}: {TemplateOutputDir}"; } } diff --git a/src/ProjectTemplates/test/RazorComponentsTemplateTest.cs b/src/ProjectTemplates/test/RazorComponentsTemplateTest.cs index 3eb5ebbcb5ec..74edc2b9444e 100644 --- a/src/ProjectTemplates/test/RazorComponentsTemplateTest.cs +++ b/src/ProjectTemplates/test/RazorComponentsTemplateTest.cs @@ -23,8 +23,8 @@ public RazorComponentsTemplateTest(ProjectFactoryFixture projectFactory, Browser public Project Project { get; private set; } - [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/8244")] - public async Task RazorComponentsTemplateWorksAsync() + [Fact] + public async Task RazorComponentsTemplateWorks() { Project = await ProjectFactory.GetOrCreateProject("razorcomponents", Output);