[Components] Relayer + Robust reconnect (#8911)

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,13 +13,16 @@
"@aspnet/signalr": "^1.0.0", "@aspnet/signalr": "^1.0.0",
"@aspnet/signalr-protocol-msgpack": "^1.0.0", "@aspnet/signalr-protocol-msgpack": "^1.0.0",
"@dotnet/jsinterop": "^0.1.1", "@dotnet/jsinterop": "^0.1.1",
"@types/jsdom": "11.0.6",
"@types/jest": "^24.0.6",
"@types/emscripten": "0.0.31", "@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", "jest": "^24.1.0",
"ts-jest": "^24.0.0", "ts-jest": "^24.0.0",
"ts-loader": "^4.4.1", "ts-loader": "^4.4.1",
"typescript": "^2.9.2", "typescript": "^3.4.0",
"webpack": "^4.12.0", "webpack": "^4.12.0",
"webpack-cli": "^3.0.8" "webpack-cli": "^3.0.8"
} }

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
export interface CircuitHandler { 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). * @param {Error} error Optionally argument containing the error that caused the connection to close (if any).
*/ */
onConnectionDown?(error?: Error): void; onConnectionDown?(error?: Error): void;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -201,6 +201,33 @@
resolved "https://registry.yarnpkg.com/@types/webassembly-js-api/-/webassembly-js-api-0.0.2.tgz#43a04bd75fa20332133c6c3986156bfeb4a3ced7" resolved "https://registry.yarnpkg.com/@types/webassembly-js-api/-/webassembly-js-api-0.0.2.tgz#43a04bd75fa20332133c6c3986156bfeb4a3ced7"
integrity sha512-htlxJRag6RUiMYUkS8Fjup+TMHO0VarpiF9MrqYaGJ0wXtIraQFz40rfA8VIeCiWy8sgpv3RLmigpgicG8fqGA== 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": "@webassemblyjs/ast@1.8.3":
version "1.8.3" version "1.8.3"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.3.tgz#63a741bd715a6b6783f2ea5c6ab707516aa215eb" 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 "^6.0.1"
acorn-walk "^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: acorn-walk@^6.0.1:
version "6.1.1" version "6.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" 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" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw== 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: ajv-errors@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" 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" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" 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" version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== 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" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w== 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: ansi-styles@^3.2.0, ansi-styles@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 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" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 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" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 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" escape-string-regexp "^1.0.5"
supports-color "^5.3.0" 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: chokidar@^2.0.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" 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" isobject "^3.0.0"
static-extend "^0.1.1" 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: cliui@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" 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: dependencies:
ms "2.0.0" 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" version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@ -1276,6 +1345,13 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0" miller-rabin "^4.0.0"
randombytes "^2.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: domain-browser@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" 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-assert "^1.0.0"
minimalistic-crypto-utils "^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: emojis-list@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" 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" esrecurse "^4.1.0"
estraverse "^4.1.1" 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: esprima@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" 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" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 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: esrecurse@^4.1.0:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
@ -1417,7 +1574,7 @@ esrecurse@^4.1.0:
dependencies: dependencies:
estraverse "^4.1.0" 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" version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
@ -1523,6 +1680,15 @@ extend@~3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 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: extglob@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" 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" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== 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: fileset@^2.0.3:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" 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" micromatch "^3.0.4"
resolve-dir "^1.0.1" 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: flush-write-stream@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" 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" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 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: gauge@~2.7.3:
version "2.7.4" version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 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" is-windows "^1.0.1"
which "^1.2.14" which "^1.2.14"
globals@^11.1.0: globals@^11.1.0, globals@^11.7.0:
version "11.11.0" version "11.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e" resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw== 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" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= 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" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -1947,6 +2146,19 @@ ignore-walk@^3.0.1:
dependencies: dependencies:
minimatch "^3.0.4" 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: import-local@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" 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" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== 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: interpret@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" 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: dependencies:
isobject "^3.0.1" 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: is-regex@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 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" argparse "^1.0.7"
esprima "^4.0.0" 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: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 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" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 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: json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 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" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
levn@~0.3.0: levn@^0.3.0, levn@~0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 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" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= 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: lodash@^4.17.10, lodash@^4.17.11:
version "4.17.11" version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 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" readable-stream "^2.3.6"
safe-buffer "^5.1.2" 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: nan@^2.9.2:
version "2.12.1" version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" 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: dependencies:
wrappy "1" 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: optimist@^0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@ -3295,7 +3561,7 @@ optimist@^0.6.1:
minimist "~0.0.1" minimist "~0.0.1"
wordwrap "~0.0.2" wordwrap "~0.0.2"
optionator@^0.8.1: optionator@^0.8.1, optionator@^0.8.2:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
@ -3333,7 +3599,7 @@ os-locale@^3.0.0:
lcid "^2.0.0" lcid "^2.0.0"
mem "^4.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" version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@ -3406,6 +3672,13 @@ parallel-transform@^1.1.0:
inherits "^2.0.3" inherits "^2.0.3"
readable-stream "^2.1.5" 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: parse-asn1@^5.0.0:
version "5.1.4" version "5.1.4"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" 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" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 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: path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 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" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= 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: promise-inflight@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" 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" extend-shallow "^3.0.2"
safe-regex "^1.1.0" 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: remove-trailing-separator@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" 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" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= 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: requires-port@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" 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" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
integrity sha1-six699nWiBvItuZTM17rywoYh0g= 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: resolve-url@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" 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: dependencies:
path-parse "^1.0.6" 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: ret@~0.1.10:
version "0.1.15" version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== 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" version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== 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" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw== 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: run-queue@^1.0.0, run-queue@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" 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: dependencies:
aproba "^1.1.1" 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: 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" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 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" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== 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: serialize-javascript@^1.4.0:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879" 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" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== 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: snapdragon-node@^2.0.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" 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" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.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" version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@ -4193,6 +4532,15 @@ string-width@^1.0.1:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.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: string_decoder@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
@ -4228,6 +4576,13 @@ strip-ansi@^5.0.0:
dependencies: dependencies:
ansi-regex "^4.0.0" 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: strip-bom@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 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" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= 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" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 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" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= 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: tapable@^1.0.0, tapable@^1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e" 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" read-pkg-up "^4.0.0"
require-main-filename "^1.0.1" 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: throat@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" 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" readable-stream "~2.3.6"
xtend "~4.0.1" 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: timers-browserify@^2.0.4:
version "2.0.10" version "2.0.10"
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
@ -4333,6 +4708,13 @@ timers-browserify@^2.0.4:
dependencies: dependencies:
setimmediate "^1.0.4" 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: tmpl@1.0.x:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" 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" micromatch "^3.1.4"
semver "^5.0.1" semver "^5.0.1"
tslib@^1.9.0: tslib@^1.8.1, tslib@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== 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: tty-browserify@0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" 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" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^2.9.2: typescript@^3.4.0:
version "2.9.2" version "3.4.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6"
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==
uglify-js@^3.1.4: uglify-js@^3.1.4:
version "3.4.9" version "3.4.9"
@ -4784,6 +5173,13 @@ write-file-atomic@2.4.1:
imurmurhash "^0.1.4" imurmurhash "^0.1.4"
signal-exit "^3.0.2" 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: ws@^5.2.0:
version "5.2.2" version "5.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
internal class RemoteJSRuntime : JSRuntimeBase internal class RemoteJSRuntime : JSRuntimeBase
{ {
private IClientProxy _clientProxy; private CircuitClientProxy _clientProxy;
internal void Initialize(CircuitClientProxy clientProxy) internal void Initialize(CircuitClientProxy clientProxy)
{ {
@ -18,11 +18,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) 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. " + throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
"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."; "prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
throw new InvalidOperationException(errorMessage); "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); _clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);

View File

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

View File

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

View File

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

View File

@ -11,10 +11,13 @@ namespace Microsoft.AspNetCore.Components.Server
public string Selector { get; set; } 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; componentType = ComponentType;
selector = Selector; selector = Selector;
prerendered = Prerendered;
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// 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. // 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; using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server namespace Microsoft.AspNetCore.Components.Server
@ -16,6 +15,6 @@ namespace Microsoft.AspNetCore.Components.Server
/// </summary> /// </summary>
/// <param name="context">The context in which the prerrendering is happening.</param> /// <param name="context">The context in which the prerrendering is happening.</param>
/// <returns><see cref="Task{TResult}"/> that will complete when the prerendering is done.</returns> /// <returns><see cref="Task{TResult}"/> that will complete when the prerendering is done.</returns>
Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context); Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext context);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,57 +3,17 @@
using System; using System;
using Microsoft.AspNetCore.Components.Services; using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
internal class HttpUriHelper : UriHelperBase 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) 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. // For now throw as we don't have a good way of aborting the request from here.
throw new InvalidOperationException( throw new InvalidOperationException("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
"Redirects are not supported on a prerendering environment."); "Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
"attempted during prerendering.");
} }
} }
} }

View File

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

View File

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

View File

@ -7,7 +7,6 @@ using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services; using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter(); var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(); var result = await helper.RenderStaticComponentAsync<TestComponent>();
result.WriteTo(writer, HtmlEncoder.Default); result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString(); var content = writer.ToString();
@ -45,7 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter(); var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new var result = await helper.RenderStaticComponentAsync<GreetingComponent>(new
{ {
Name = "Steve" Name = "Steve"
}); });
@ -63,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper(); var helper = CreateHelper();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{ {
IsAsync = false IsAsync = false
})); }));
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper(); var helper = CreateHelper();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{ {
IsAsync = true IsAsync = true
})); }));
@ -95,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper(); var helper = CreateHelper();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
{ {
JsInterop = true JsInterop = true
})); }));
@ -122,16 +121,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter(); var writer = new StringWriter();
// Act // Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<RedirectComponent>(new
{ {
RedirectUri = "http://localhost/redirect" 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] [Fact]
public async Task CanRender_AsyncComponent() public async Task CanRender_AsyncComponent()
{ {
@ -182,7 +182,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>"; </table>";
// Act // Act
var result = await helper.RenderComponentAsync<AsyncComponent>(); var result = await helper.RenderStaticComponentAsync<AsyncComponent>();
result.WriteTo(writer, HtmlEncoder.Default); result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString(); var content = writer.ToString();
@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
services.AddSingleton(HtmlEncoder.Default); services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<IJSRuntime,UnsupportedJavaScriptRuntime>(); services.AddSingleton<IJSRuntime,UnsupportedJavaScriptRuntime>();
services.AddSingleton<IUriHelper,HttpUriHelper>(); services.AddSingleton<IUriHelper,HttpUriHelper>();
services.AddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>(); services.AddSingleton<StaticComponentRenderer>();
configureServices?.Invoke(services); configureServices?.Invoke(services);

View File

@ -317,6 +317,12 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpWebSite", "test\WebSi
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{A8C3066F-E80D-4E03-9962-741B551B8FBC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{A8C3066F-E80D-4E03-9962-741B551B8FBC}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|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.ActiveCfg = Release|Any CPU
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|x86.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -1938,6 +1968,8 @@ Global
{F99CAC82-C96E-41F4-AA28-1BBBD09C447A} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4} {F99CAC82-C96E-41F4-AA28-1BBBD09C447A} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
{65E98187-96FB-4FCD-94A3-F8048C2F13F1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {65E98187-96FB-4FCD-94A3-F8048C2F13F1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A8C3066F-E80D-4E03-9962-741B551B8FBC} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A} SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers namespace BasicWebSite.Controllers
@ -50,11 +51,16 @@ namespace BasicWebSite.Controllers
} }
}; };
[HttpGet("/components")] [HttpGet("/components/{staticPrerender=true}")]
[HttpGet("/components/{component}")] [HttpGet("/components/routable/{staticPrerender=true}")]
public IActionResult Index() 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")] [HttpGet("/WeatherData")]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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