[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:
parent
3cc3ab00c9
commit
8499a27c7f
|
|
@ -96,6 +96,7 @@
|
|||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Razor" ProjectPath="$(RepositoryRoot)src\Razor\Razor\src\Microsoft.AspNetCore.Razor.csproj" RefProjectPath="$(RepositoryRoot)src\Razor\Razor\ref\Microsoft.AspNetCore.Razor.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Abstractions" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\src\Microsoft.AspNetCore.Mvc.Abstractions.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Abstractions\ref\Microsoft.AspNetCore.Mvc.Abstractions.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.ApiExplorer" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\src\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.ApiExplorer\ref\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\ref\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Core" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Core\ref\Microsoft.AspNetCore.Mvc.Core.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Cors" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\src\Microsoft.AspNetCore.Mvc.Cors.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.Cors\ref\Microsoft.AspNetCore.Mvc.Cors.csproj" />
|
||||
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.DataAnnotations" ProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\src\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" RefProjectPath="$(RepositoryRoot)src\Mvc\Mvc.DataAnnotations\ref\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" />
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
|
|||
{
|
||||
internal WebAssemblyUriHelper() { }
|
||||
public static readonly Microsoft.AspNetCore.Blazor.Services.WebAssemblyUriHelper Instance;
|
||||
protected override void InitializeState() { }
|
||||
protected override void EnsureInitialized() { }
|
||||
protected override void NavigateToCore(string uri, bool forceLoad) { }
|
||||
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
|
||||
public static void NotifyLocationChanged(string newAbsoluteUri) { }
|
||||
|
|
|
|||
|
|
@ -25,11 +25,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to initialize BaseURI and current URI before those values are used the first time.
|
||||
/// Override this method to dynamically calculate those values.
|
||||
/// </summary>
|
||||
protected override void InitializeState()
|
||||
protected override void EnsureInitialized()
|
||||
{
|
||||
WebAssemblyJSRuntime.Instance.Invoke<object>(
|
||||
Interop.EnableNavigationInterception,
|
||||
|
|
@ -40,8 +36,7 @@ namespace Microsoft.AspNetCore.Blazor.Services
|
|||
// client-side (Mono) use, so it's OK to rely on synchronicity here.
|
||||
var baseUri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetBaseUri);
|
||||
var uri = WebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetLocationHref);
|
||||
SetAbsoluteBaseUri(baseUri);
|
||||
SetAbsoluteUri(uri);
|
||||
InitializeState(uri, baseUri);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
|
||||
protected override void HandleException(Exception exception)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
ExceptionDispatchInfo.Capture(exception).Throw();
|
||||
}
|
||||
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@
|
|||
"@aspnet/signalr": "^1.0.0",
|
||||
"@aspnet/signalr-protocol-msgpack": "^1.0.0",
|
||||
"@dotnet/jsinterop": "^0.1.1",
|
||||
"@types/jsdom": "11.0.6",
|
||||
"@types/jest": "^24.0.6",
|
||||
"@types/emscripten": "0.0.31",
|
||||
"@types/jest": "^24.0.6",
|
||||
"@types/jsdom": "11.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^1.5.0",
|
||||
"@typescript-eslint/parser": "^1.5.0",
|
||||
"eslint": "^5.16.0",
|
||||
"jest": "^24.1.0",
|
||||
"ts-jest": "^24.0.0",
|
||||
"ts-loader": "^4.4.1",
|
||||
"typescript": "^2.9.2",
|
||||
"typescript": "^3.4.0",
|
||||
"webpack": "^4.12.0",
|
||||
"webpack-cli": "^3.0.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
};
|
||||
|
|
@ -2,15 +2,27 @@ import '@dotnet/jsinterop';
|
|||
import './GlobalExports';
|
||||
import * as signalR from '@aspnet/signalr';
|
||||
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
|
||||
import { OutOfProcessRenderBatch } from './Rendering/RenderBatch/OutOfProcessRenderBatch';
|
||||
import { internalFunctions as uriHelperFunctions } from './Services/UriHelper';
|
||||
import { renderBatch } from './Rendering/Renderer';
|
||||
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync } from './BootCommon';
|
||||
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
|
||||
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
|
||||
import RenderQueue from './Platform/Circuits/RenderQueue';
|
||||
import { ConsoleLogger } from './Platform/Logging/Loggers';
|
||||
import { LogLevel, ILogger } from './Platform/Logging/ILogger';
|
||||
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
|
||||
|
||||
async function boot() {
|
||||
const circuitHandlers: CircuitHandler[] = [ new AutoReconnectCircuitHandler() ];
|
||||
let renderingFailed = false;
|
||||
|
||||
async function boot(): Promise<void> {
|
||||
|
||||
// For development.
|
||||
// Simply put a break point here and modify the log level during
|
||||
// development to get traces.
|
||||
// In the future we will allow for users to configure this.
|
||||
const logger = new ConsoleLogger(LogLevel.Error);
|
||||
|
||||
logger.log(LogLevel.Information, 'Booting blazor.');
|
||||
|
||||
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
|
||||
window['Blazor'].circuitHandlers = circuitHandlers;
|
||||
|
||||
// In the background, start loading the boot config and any embedded resources
|
||||
|
|
@ -18,19 +30,35 @@ async function boot() {
|
|||
return loadEmbeddedResourcesAsync(bootConfig);
|
||||
});
|
||||
|
||||
const initialConnection = await initializeConnection(circuitHandlers);
|
||||
const initialConnection = await initializeConnection(circuitHandlers, logger);
|
||||
|
||||
const circuits = discoverPrerenderedCircuits(document);
|
||||
for (let i = 0; i < circuits.length; i++) {
|
||||
const circuit = circuits[i];
|
||||
for (let j = 0; j < circuit.components.length; j++) {
|
||||
const component = circuit.components[j];
|
||||
component.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure any embedded resources have been loaded before starting the app
|
||||
await embeddedResourcesPromise;
|
||||
const circuitId = await initialConnection.invoke<string>(
|
||||
'StartCircuit',
|
||||
uriHelperFunctions.getLocationHref(),
|
||||
uriHelperFunctions.getBaseURI()
|
||||
);
|
||||
|
||||
window['Blazor'].reconnect = async () => {
|
||||
const reconnection = await initializeConnection(circuitHandlers);
|
||||
if (!(await reconnection.invoke<Boolean>('ConnectCircuit', circuitId))) {
|
||||
const circuit = await startCircuit(initialConnection);
|
||||
|
||||
if (!circuit) {
|
||||
logger.log(LogLevel.Information, 'No preregistered components to render.');
|
||||
}
|
||||
|
||||
const reconnect = async (): Promise<boolean> => {
|
||||
if (renderingFailed) {
|
||||
// We can't reconnect after a failure, so exit early.
|
||||
return false;
|
||||
}
|
||||
const reconnection = await initializeConnection(circuitHandlers, logger);
|
||||
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
|
||||
|
||||
if (reconnectionFailed(results)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -38,10 +66,22 @@ async function boot() {
|
|||
return true;
|
||||
};
|
||||
|
||||
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
|
||||
window['Blazor'].reconnect = reconnect;
|
||||
|
||||
const reconnectTask = reconnect();
|
||||
|
||||
if (circuit) {
|
||||
circuits.push(circuit);
|
||||
}
|
||||
|
||||
await reconnectTask;
|
||||
|
||||
function reconnectionFailed(results: boolean[]): boolean {
|
||||
return !results.reduce((current, next) => current && next, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<signalR.HubConnection> {
|
||||
async function initializeConnection(circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
|
||||
const hubProtocol = new MessagePackHubProtocol();
|
||||
(hubProtocol as any).name = 'blazorpack';
|
||||
const connection = new signalR.HubConnectionBuilder()
|
||||
|
|
@ -51,44 +91,42 @@ async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<
|
|||
.build();
|
||||
|
||||
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
|
||||
connection.on('JS.RenderBatch', (browserRendererId: number, renderId: number, batchData: Uint8Array) => {
|
||||
try {
|
||||
renderBatch(browserRendererId, new OutOfProcessRenderBatch(batchData));
|
||||
connection.send('OnRenderCompleted', renderId, null);
|
||||
} catch (ex) {
|
||||
// If there's a rendering exception, notify server *and* throw on client
|
||||
connection.send('OnRenderCompleted', renderId, ex.toString());
|
||||
throw ex;
|
||||
}
|
||||
connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => {
|
||||
logger.log(LogLevel.Information, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`);
|
||||
|
||||
const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger);
|
||||
|
||||
queue.processBatch(batchId, batchData, connection);
|
||||
});
|
||||
|
||||
connection.onclose(error => circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
|
||||
connection.on('JS.Error', error => unhandledError(connection, error));
|
||||
connection.onclose(error => !renderingFailed && circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
|
||||
connection.on('JS.Error', error => unhandledError(connection, error, logger));
|
||||
|
||||
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
|
||||
|
||||
try {
|
||||
await connection.start();
|
||||
} catch (ex) {
|
||||
unhandledError(connection, ex);
|
||||
unhandledError(connection, ex, logger);
|
||||
}
|
||||
|
||||
DotNet.attachDispatcher({
|
||||
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson) => {
|
||||
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
function unhandledError(connection: signalR.HubConnection, err: Error) {
|
||||
console.error(err);
|
||||
function unhandledError(connection: signalR.HubConnection, err: Error, logger: ILogger): void {
|
||||
logger.log(LogLevel.Error, err);
|
||||
|
||||
// Disconnect on errors.
|
||||
//
|
||||
// Trying to call methods on the connection after its been closed will throw.
|
||||
if (connection) {
|
||||
renderingFailed = true;
|
||||
connection.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ import { CircuitHandler } from './CircuitHandler';
|
|||
import { UserSpecifiedDisplay } from './UserSpecifiedDisplay';
|
||||
import { DefaultReconnectDisplay } from './DefaultReconnectDisplay';
|
||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { ILogger, LogLevel } from '../Logging/ILogger';
|
||||
export class AutoReconnectCircuitHandler implements CircuitHandler {
|
||||
static readonly MaxRetries = 5;
|
||||
static readonly RetryInterval = 3000;
|
||||
static readonly DialogId = 'components-reconnect-modal';
|
||||
reconnectDisplay: ReconnectDisplay;
|
||||
public static readonly MaxRetries = 5;
|
||||
public static readonly RetryInterval = 3000;
|
||||
public static readonly DialogId = 'components-reconnect-modal';
|
||||
public reconnectDisplay: ReconnectDisplay;
|
||||
public logger: ILogger;
|
||||
|
||||
constructor() {
|
||||
public constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
this.reconnectDisplay = new DefaultReconnectDisplay(document);
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const modal = document.getElementById(AutoReconnectCircuitHandler.DialogId);
|
||||
|
|
@ -17,15 +20,15 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
|
|||
}
|
||||
});
|
||||
}
|
||||
onConnectionUp() : void{
|
||||
public onConnectionUp(): void {
|
||||
this.reconnectDisplay.hide();
|
||||
}
|
||||
|
||||
delay() : Promise<void>{
|
||||
public delay(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, AutoReconnectCircuitHandler.RetryInterval));
|
||||
}
|
||||
|
||||
async onConnectionDown() : Promise<void> {
|
||||
public async onConnectionDown(): Promise<void> {
|
||||
this.reconnectDisplay.show();
|
||||
|
||||
for (let i = 0; i < AutoReconnectCircuitHandler.MaxRetries; i++) {
|
||||
|
|
@ -38,7 +41,7 @@ export class AutoReconnectCircuitHandler implements CircuitHandler {
|
|||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.logger.log(LogLevel.Error, err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
export interface CircuitHandler {
|
||||
/** Invoked when a server connection is established or re-established after a connection failure.
|
||||
/** Invoked when a server connection is established or re-established after a connection failure.
|
||||
*/
|
||||
onConnectionUp?() : void;
|
||||
onConnectionUp?(): void;
|
||||
|
||||
/** Invoked when a server connection is dropped.
|
||||
/** Invoked when a server connection is dropped.
|
||||
* @param {Error} error Optionally argument containing the error that caused the connection to close (if any).
|
||||
*/
|
||||
onConnectionDown?(error?: Error): void;
|
||||
onConnectionDown?(error?: Error): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { ReconnectDisplay } from "./ReconnectDisplay";
|
||||
import { AutoReconnectCircuitHandler } from "./AutoReconnectCircuitHandler";
|
||||
import { ReconnectDisplay } from './ReconnectDisplay';
|
||||
import { AutoReconnectCircuitHandler } from './AutoReconnectCircuitHandler';
|
||||
export class DefaultReconnectDisplay implements ReconnectDisplay {
|
||||
modal: HTMLDivElement;
|
||||
message: HTMLHeadingElement;
|
||||
|
|
@ -10,18 +10,18 @@ export class DefaultReconnectDisplay implements ReconnectDisplay {
|
|||
this.modal.id = AutoReconnectCircuitHandler.DialogId;
|
||||
|
||||
const modalStyles = [
|
||||
"position: fixed",
|
||||
"top: 0",
|
||||
"right: 0",
|
||||
"bottom: 0",
|
||||
"left: 0",
|
||||
"z-index: 1000",
|
||||
"display: none",
|
||||
"overflow: hidden",
|
||||
"background-color: #fff",
|
||||
"opacity: 0.8",
|
||||
"text-align: center",
|
||||
"font-weight: bold"
|
||||
'position: fixed',
|
||||
'top: 0',
|
||||
'right: 0',
|
||||
'bottom: 0',
|
||||
'left: 0',
|
||||
'z-index: 1000',
|
||||
'display: none',
|
||||
'overflow: hidden',
|
||||
'background-color: #fff',
|
||||
'opacity: 0.8',
|
||||
'text-align: center',
|
||||
'font-weight: bold'
|
||||
];
|
||||
|
||||
this.modal.style.cssText = modalStyles.join(';');
|
||||
|
|
|
|||
|
|
@ -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}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,32 @@
|
|||
import { RenderBatch, ArraySegment, RenderTreeEdit, RenderTreeFrame, EditType, FrameType, ArrayValues } from './RenderBatch/RenderBatch';
|
||||
import { EventDelegator } from './EventDelegator';
|
||||
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
|
||||
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
|
||||
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd } from './LogicalElements';
|
||||
import { applyCaptureIdToElement } from './ElementReferenceCapture';
|
||||
const selectValuePropname = '_blazorSelectValue';
|
||||
const sharedTemplateElemForParsing = document.createElement('template');
|
||||
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true };
|
||||
const rootComponentsPendingFirstRender: { [componentId: number]: Element } = {};
|
||||
const rootComponentsPendingFirstRender: { [componentId: number]: LogicalElement } = {};
|
||||
|
||||
export class BrowserRenderer {
|
||||
private eventDelegator: EventDelegator;
|
||||
private childComponentLocations: { [componentId: number]: LogicalElement } = {};
|
||||
private browserRendererId: number;
|
||||
|
||||
constructor(private browserRendererId: number) {
|
||||
public constructor(browserRendererId: number) {
|
||||
this.browserRendererId = browserRendererId;
|
||||
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs) => {
|
||||
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs);
|
||||
});
|
||||
}
|
||||
|
||||
public attachRootComponentToElement(componentId: number, element: Element) {
|
||||
// 'allowExistingContents' to keep any prerendered content until we do the first client-side render
|
||||
this.attachComponentToElement(componentId, toLogicalElement(element, /* allowExistingContents */ true));
|
||||
public attachRootComponentToLogicalElement(componentId: number, element: LogicalElement): void {
|
||||
this.attachComponentToElement(componentId, element);
|
||||
rootComponentsPendingFirstRender[componentId] = element;
|
||||
}
|
||||
|
||||
public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
|
||||
public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>): void {
|
||||
const element = this.childComponentLocations[componentId];
|
||||
if (!element) {
|
||||
throw new Error(`No element is currently associated with component ${componentId}`);
|
||||
|
|
@ -34,8 +35,14 @@ export class BrowserRenderer {
|
|||
// On the first render for each root component, clear any existing content (e.g., prerendered)
|
||||
const rootElementToClear = rootComponentsPendingFirstRender[componentId];
|
||||
if (rootElementToClear) {
|
||||
const rootElementToClearEnd = getLogicalSiblingEnd(rootElementToClear);
|
||||
delete rootComponentsPendingFirstRender[componentId];
|
||||
clearElement(rootElementToClear);
|
||||
|
||||
if (!rootElementToClearEnd) {
|
||||
clearElement(rootElementToClear as unknown as Element);
|
||||
} else {
|
||||
clearBetween(rootElementToClear as unknown as Node, rootElementToClearEnd as unknown as Comment);
|
||||
}
|
||||
}
|
||||
|
||||
this.applyEdits(batch, element, 0, edits, referenceFrames);
|
||||
|
|
@ -89,7 +96,7 @@ export class BrowserRenderer {
|
|||
if (element instanceof Element) {
|
||||
this.applyAttribute(batch, element, frame);
|
||||
} else {
|
||||
throw new Error(`Cannot set attribute on non-element child`);
|
||||
throw new Error('Cannot set attribute on non-element child');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -106,7 +113,7 @@ export class BrowserRenderer {
|
|||
element.removeAttribute(attributeName);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Cannot remove attribute from non-element child`);
|
||||
throw new Error('Cannot remove attribute from non-element child');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -118,7 +125,7 @@ export class BrowserRenderer {
|
|||
if (textNode instanceof Text) {
|
||||
textNode.textContent = frameReader.textContent(frame);
|
||||
} else {
|
||||
throw new Error(`Cannot set text content on non-text child`);
|
||||
throw new Error('Cannot set text content on non-text child');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -336,6 +343,11 @@ export class BrowserRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
export interface ComponentDescriptor {
|
||||
start: Node;
|
||||
end: Node;
|
||||
}
|
||||
|
||||
function parseMarkup(markup: string, isSvg: boolean) {
|
||||
if (isSvg) {
|
||||
sharedSvgElemForParsing.innerHTML = markup || ' ';
|
||||
|
|
@ -369,7 +381,7 @@ function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: num
|
|||
const eventDescriptor = {
|
||||
browserRendererId,
|
||||
eventHandlerId,
|
||||
eventArgsType: eventArgs.type
|
||||
eventArgsType: eventArgs.type,
|
||||
};
|
||||
|
||||
return DotNet.invokeMethodAsync(
|
||||
|
|
@ -385,3 +397,22 @@ function clearElement(element: Element) {
|
|||
element.removeChild(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
function clearBetween(start: Node, end: Node): void {
|
||||
const logicalParent = getLogicalParent(start as unknown as LogicalElement);
|
||||
if(!logicalParent){
|
||||
throw new Error("Can't clear between nodes. The start node does not have a logical parent.");
|
||||
}
|
||||
const children = getLogicalChildrenArray(logicalParent);
|
||||
const removeStart = children.indexOf(start as unknown as LogicalElement) + 1;
|
||||
const endIndex = children.indexOf(end as unknown as LogicalElement);
|
||||
|
||||
// We remove the end component comment from the DOM as we don't need it after this point.
|
||||
for (let i = removeStart; i <= endIndex; i++) {
|
||||
removeLogicalChild(logicalParent, removeStart);
|
||||
}
|
||||
|
||||
// We sanitize the start comment by removing all the information from it now that we don't need it anymore
|
||||
// as it adds noise to the DOM.
|
||||
start.textContent = '!';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
A LogicalElement plays the same role as an Element instance from the point of view of the
|
||||
API consumer. Inserting and removing logical elements updates the browser DOM just the same.
|
||||
|
||||
|
|
@ -27,8 +27,40 @@
|
|||
|
||||
const logicalChildrenPropname = createSymbolOrFallback('_blazorLogicalChildren');
|
||||
const logicalParentPropname = createSymbolOrFallback('_blazorLogicalParent');
|
||||
const logicalEndSiblingPropname = createSymbolOrFallback('_blazorLogicalEnd');
|
||||
|
||||
export function toLogicalElement(element: Element, allowExistingContents?: boolean) {
|
||||
export function toLogicalRootCommentElement(start: Comment, end: Comment): LogicalElement {
|
||||
// Now that we support start/end comments as component delimiters we are going to be setting up
|
||||
// adding the components rendered output as siblings of the start/end tags (between).
|
||||
// For that to work, we need to appropriately configure the parent element to be a logical element
|
||||
// with all their children being the child elements.
|
||||
// For example, imagine you have
|
||||
// <app>
|
||||
// <div><p>Static content</p></div>
|
||||
// <!-- start component
|
||||
// <!-- end component
|
||||
// <footer>Some other content</footer>
|
||||
// <app>
|
||||
// We want the parent element to be something like
|
||||
// *app
|
||||
// |- *div
|
||||
// |- *component
|
||||
// |- *footer
|
||||
if(!start.parentNode){
|
||||
throw new Error(`Comment not connected to the DOM ${start.textContent}`);
|
||||
}
|
||||
|
||||
const parent = start.parentNode;
|
||||
const parentLogicalElement = toLogicalElement(parent, /* allow existing contents */ true);
|
||||
const children = getLogicalChildrenArray(parentLogicalElement);
|
||||
Array.from(parent.childNodes).forEach(n => children.push(n as unknown as LogicalElement));
|
||||
start[logicalParentPropname] = parentLogicalElement;
|
||||
start[logicalEndSiblingPropname] = end;
|
||||
toLogicalElement(end, /* allowExistingcontents */ true);
|
||||
return toLogicalElement(start, /* allowExistingContents */ true);
|
||||
}
|
||||
|
||||
export function toLogicalElement(element: Node, allowExistingContents?: boolean): LogicalElement {
|
||||
// Normally it's good to assert that the element has started empty, because that's the usual
|
||||
// situation and we probably have a bug if it's not. But for the element that contain prerendered
|
||||
// root components, we want to let them keep their content until we replace it.
|
||||
|
|
@ -37,7 +69,7 @@ export function toLogicalElement(element: Element, allowExistingContents?: boole
|
|||
}
|
||||
|
||||
element[logicalChildrenPropname] = [];
|
||||
return element as any as LogicalElement;
|
||||
return element as unknown as LogicalElement;
|
||||
}
|
||||
|
||||
export function createAndInsertLogicalContainer(parent: LogicalElement, childIndex: number): LogicalElement {
|
||||
|
|
@ -107,6 +139,10 @@ export function getLogicalParent(element: LogicalElement): LogicalElement | null
|
|||
return (element[logicalParentPropname] as LogicalElement) || null;
|
||||
}
|
||||
|
||||
export function getLogicalSiblingEnd(element: LogicalElement): LogicalElement | null {
|
||||
return (element[logicalEndSiblingPropname] as LogicalElement) || null;
|
||||
}
|
||||
|
||||
export function getLogicalChild(parent: LogicalElement, childIndex: number): LogicalElement {
|
||||
return getLogicalChildrenArray(parent)[childIndex];
|
||||
}
|
||||
|
|
@ -115,7 +151,7 @@ export function isSvgElement(element: LogicalElement) {
|
|||
return getClosestDomElement(element).namespaceURI === 'http://www.w3.org/2000/svg';
|
||||
}
|
||||
|
||||
function getLogicalChildrenArray(element: LogicalElement) {
|
||||
export function getLogicalChildrenArray(element: LogicalElement) {
|
||||
return element[logicalChildrenPropname] as LogicalElement[];
|
||||
}
|
||||
|
||||
|
|
@ -161,4 +197,4 @@ function createSymbolOrFallback(fallback: string): symbol | string {
|
|||
}
|
||||
|
||||
// Nominal type to represent a logical element without needing to allocate any object for instances
|
||||
export interface LogicalElement { LogicalElement__DO_NOT_IMPLEMENT: any };
|
||||
export interface LogicalElement { LogicalElement__DO_NOT_IMPLEMENT: any }
|
||||
|
|
|
|||
|
|
@ -1,25 +1,37 @@
|
|||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { System_Object, System_String, System_Array, MethodHandle, Pointer } from '../Platform/Platform';
|
||||
import { platform } from '../Environment';
|
||||
import { RenderBatch } from './RenderBatch/RenderBatch';
|
||||
import { BrowserRenderer } from './BrowserRenderer';
|
||||
import { toLogicalElement, LogicalElement } from './LogicalElements';
|
||||
|
||||
type BrowserRendererRegistry = { [browserRendererId: number]: BrowserRenderer };
|
||||
interface BrowserRendererRegistry {
|
||||
[browserRendererId: number]: BrowserRenderer;
|
||||
}
|
||||
const browserRenderers: BrowserRendererRegistry = {};
|
||||
|
||||
export function attachRootComponentToElement(browserRendererId: number, elementSelector: string, componentId: number) {
|
||||
const element = document.querySelector(elementSelector);
|
||||
if (!element) {
|
||||
throw new Error(`Could not find any element matching selector '${elementSelector}'.`);
|
||||
}
|
||||
export function attachRootComponentToLogicalElement(browserRendererId: number, logicalElement: LogicalElement, componentId: number): void {
|
||||
|
||||
let browserRenderer = browserRenderers[browserRendererId];
|
||||
if (!browserRenderer) {
|
||||
browserRenderer = browserRenderers[browserRendererId] = new BrowserRenderer(browserRendererId);
|
||||
}
|
||||
browserRenderer.attachRootComponentToElement(componentId, element);
|
||||
|
||||
browserRenderer.attachRootComponentToLogicalElement(componentId, logicalElement);
|
||||
}
|
||||
|
||||
export function renderBatch(browserRendererId: number, batch: RenderBatch) {
|
||||
export function attachRootComponentToElement(browserRendererId: number, elementSelector: string, componentId: number): void {
|
||||
|
||||
const element = document.querySelector(elementSelector);
|
||||
if (!element) {
|
||||
throw new Error(`Could not find any element matching selector '${elementSelector}'.`);
|
||||
}
|
||||
|
||||
// 'allowExistingContents' to keep any prerendered content until we do the first client-side render
|
||||
attachRootComponentToLogicalElement(browserRendererId, toLogicalElement(element, /* allow existing contents */ true), componentId);
|
||||
}
|
||||
|
||||
export function renderBatch(browserRendererId: number, batch: RenderBatch): void {
|
||||
const browserRenderer = browserRenderers[browserRendererId];
|
||||
if (!browserRenderer) {
|
||||
throw new Error(`There is no browser renderer with ID ${browserRendererId}.`);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json"
|
||||
"extends": "../tsconfig.json"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { AutoReconnectCircuitHandler } from "../src/Platform/Circuits/AutoReconn
|
|||
import { UserSpecifiedDisplay } from "../src/Platform/Circuits/UserSpecifiedDisplay";
|
||||
import { DefaultReconnectDisplay } from "../src/Platform/Circuits/DefaultReconnectDisplay";
|
||||
import { ReconnectDisplay } from "../src/Platform/Circuits/ReconnectDisplay";
|
||||
import { NullLogger} from '../src/Platform/Logging/Loggers';
|
||||
import '../src/GlobalExports';
|
||||
|
||||
describe('AutoReconnectCircuitHandler', () => {
|
||||
it('creates default element', () => {
|
||||
const handler = new AutoReconnectCircuitHandler();
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
|
||||
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||||
expect(handler.reconnectDisplay).toBeInstanceOf(DefaultReconnectDisplay);
|
||||
|
|
@ -17,7 +18,7 @@ describe('AutoReconnectCircuitHandler', () => {
|
|||
const element = document.createElement('div');
|
||||
element.id = 'components-reconnect-modal';
|
||||
document.body.appendChild(element);
|
||||
const handler = new AutoReconnectCircuitHandler();
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
|
||||
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||||
expect(handler.reconnectDisplay).toBeInstanceOf(UserSpecifiedDisplay);
|
||||
|
|
@ -32,7 +33,7 @@ describe('AutoReconnectCircuitHandler', () => {
|
|||
}));
|
||||
|
||||
it('hides display on connection up', () => {
|
||||
const handler = new AutoReconnectCircuitHandler();
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
const testDisplay = new TestDisplay();
|
||||
handler.reconnectDisplay = testDisplay;
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ describe('AutoReconnectCircuitHandler', () => {
|
|||
});
|
||||
|
||||
it('shows display on connection down', async () => {
|
||||
const handler = new AutoReconnectCircuitHandler();
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
handler.delay = () => Promise.resolve();
|
||||
const reconnect = jest.fn().mockResolvedValue(true);
|
||||
window['Blazor'].reconnect = reconnect;
|
||||
|
|
@ -59,7 +60,7 @@ describe('AutoReconnectCircuitHandler', () => {
|
|||
});
|
||||
|
||||
it('invokes failed if reconnect fails', async () => {
|
||||
const handler = new AutoReconnectCircuitHandler();
|
||||
const handler = new AutoReconnectCircuitHandler(NullLogger.instance);
|
||||
handler.delay = () => Promise.resolve();
|
||||
const reconnect = jest.fn().mockRejectedValue(new Error('some error'));
|
||||
window.console.error = jest.fn();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json"
|
||||
"extends": "../tsconfig.json",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"downlevelIteration": true,
|
||||
"target": "es5",
|
||||
"lib": ["es2015", "dom"],
|
||||
"strict": true
|
||||
|
|
@ -201,6 +201,33 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/webassembly-js-api/-/webassembly-js-api-0.0.2.tgz#43a04bd75fa20332133c6c3986156bfeb4a3ced7"
|
||||
integrity sha512-htlxJRag6RUiMYUkS8Fjup+TMHO0VarpiF9MrqYaGJ0wXtIraQFz40rfA8VIeCiWy8sgpv3RLmigpgicG8fqGA==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.5.0.tgz#85c509bcfc2eb35f37958fa677379c80b7a8f66f"
|
||||
integrity sha512-TZ5HRDFz6CswqBUviPX8EfS+iOoGbclYroZKT3GWGYiGScX0qo6QjHc5uuM7JN920voP2zgCkHgF5SDEVlCtjQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/parser" "1.5.0"
|
||||
"@typescript-eslint/typescript-estree" "1.5.0"
|
||||
requireindex "^1.2.0"
|
||||
tsutils "^3.7.0"
|
||||
|
||||
"@typescript-eslint/parser@1.5.0", "@typescript-eslint/parser@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.5.0.tgz#a96114d195dff2a49534e4c4850fb676f905a072"
|
||||
integrity sha512-pRWTnJrnxuT0ragdY26hZL+bxqDd4liMlftpH2CBlMPryOIOb1J+MdZuw6R4tIu6bWVdwbHKPTs+Q34LuGvfGw==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "1.5.0"
|
||||
eslint-scope "^4.0.0"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.5.0.tgz#986b356ecdf5a0c3bc9889d221802149cf5dbd4e"
|
||||
integrity sha512-XqR14d4BcYgxcrpxIwcee7UEjncl9emKc/MgkeUfIk2u85KlsGYyaxC7Zxjmb17JtWERk/NaO+KnBsqgpIXzwA==
|
||||
dependencies:
|
||||
lodash.unescape "4.0.1"
|
||||
semver "5.5.0"
|
||||
|
||||
"@webassemblyjs/ast@1.8.3":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.3.tgz#63a741bd715a6b6783f2ea5c6ab707516aa215eb"
|
||||
|
|
@ -380,6 +407,11 @@ acorn-globals@^4.1.0:
|
|||
acorn "^6.0.1"
|
||||
acorn-walk "^6.0.1"
|
||||
|
||||
acorn-jsx@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e"
|
||||
integrity sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==
|
||||
|
||||
acorn-walk@^6.0.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913"
|
||||
|
|
@ -395,6 +427,11 @@ acorn@^6.0.1, acorn@^6.0.5:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.0.tgz#b0a3be31752c97a0f7013c5f4903b71a05db6818"
|
||||
integrity sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==
|
||||
|
||||
acorn@^6.0.7:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
|
||||
integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
|
||||
|
||||
ajv-errors@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
||||
|
|
@ -415,7 +452,17 @@ ajv@^6.1.0, ajv@^6.5.5:
|
|||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-escapes@^3.0.0:
|
||||
ajv@^6.9.1:
|
||||
version "6.10.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
|
||||
integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
|
||||
dependencies:
|
||||
fast-deep-equal "^2.0.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-escapes@^3.0.0, ansi-escapes@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
|
||||
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
|
||||
|
|
@ -435,6 +482,11 @@ ansi-regex@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9"
|
||||
integrity sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==
|
||||
|
||||
ansi-regex@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
||||
|
||||
ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
|
|
@ -872,7 +924,7 @@ caseless@~0.12.0:
|
|||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
|
||||
chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
|
|
@ -881,6 +933,11 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chardet@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
chokidar@^2.0.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058"
|
||||
|
|
@ -935,6 +992,18 @@ class-utils@^0.3.5:
|
|||
isobject "^3.0.0"
|
||||
static-extend "^0.1.1"
|
||||
|
||||
cli-cursor@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
|
||||
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-width@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
|
||||
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
|
||||
|
||||
cliui@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
|
||||
|
|
@ -1166,7 +1235,7 @@ debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^4.1.0, debug@^4.1.1:
|
||||
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
|
|
@ -1276,6 +1345,13 @@ diffie-hellman@^5.0.0:
|
|||
miller-rabin "^4.0.0"
|
||||
randombytes "^2.0.0"
|
||||
|
||||
doctrine@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
|
||||
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
domain-browser@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||
|
|
@ -1319,6 +1395,11 @@ elliptic@^6.0.0:
|
|||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.0"
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
|
|
@ -1400,6 +1481,75 @@ eslint-scope@^4.0.0:
|
|||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-scope@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
|
||||
integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
|
||||
dependencies:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-utils@^1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
|
||||
integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
|
||||
|
||||
eslint-visitor-keys@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
|
||||
integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
|
||||
|
||||
eslint@^5.16.0:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
|
||||
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
ajv "^6.9.1"
|
||||
chalk "^2.1.0"
|
||||
cross-spawn "^6.0.5"
|
||||
debug "^4.0.1"
|
||||
doctrine "^3.0.0"
|
||||
eslint-scope "^4.0.3"
|
||||
eslint-utils "^1.3.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
espree "^5.0.1"
|
||||
esquery "^1.0.1"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^5.0.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob "^7.1.2"
|
||||
globals "^11.7.0"
|
||||
ignore "^4.0.6"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^6.2.2"
|
||||
js-yaml "^3.13.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.17.11"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^0.5.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.8.2"
|
||||
path-is-inside "^1.0.2"
|
||||
progress "^2.0.0"
|
||||
regexpp "^2.0.1"
|
||||
semver "^5.5.1"
|
||||
strip-ansi "^4.0.0"
|
||||
strip-json-comments "^2.0.1"
|
||||
table "^5.2.3"
|
||||
text-table "^0.2.0"
|
||||
|
||||
espree@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
|
||||
integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==
|
||||
dependencies:
|
||||
acorn "^6.0.7"
|
||||
acorn-jsx "^5.0.0"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
|
||||
esprima@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
|
|
@ -1410,6 +1560,13 @@ esprima@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esquery@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
|
||||
integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
|
||||
dependencies:
|
||||
estraverse "^4.0.0"
|
||||
|
||||
esrecurse@^4.1.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
|
||||
|
|
@ -1417,7 +1574,7 @@ esrecurse@^4.1.0:
|
|||
dependencies:
|
||||
estraverse "^4.1.0"
|
||||
|
||||
estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
|
||||
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
|
||||
|
|
@ -1523,6 +1680,15 @@ extend@~3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
external-editor@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
|
||||
integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==
|
||||
dependencies:
|
||||
chardet "^0.7.0"
|
||||
iconv-lite "^0.4.24"
|
||||
tmp "^0.0.33"
|
||||
|
||||
extglob@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
|
||||
|
|
@ -1574,6 +1740,20 @@ figgy-pudding@^3.5.1:
|
|||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
|
||||
integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
|
||||
dependencies:
|
||||
escape-string-regexp "^1.0.5"
|
||||
|
||||
file-entry-cache@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c"
|
||||
integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==
|
||||
dependencies:
|
||||
flat-cache "^2.0.1"
|
||||
|
||||
fileset@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
|
||||
|
|
@ -1618,6 +1798,20 @@ findup-sync@^2.0.0:
|
|||
micromatch "^3.0.4"
|
||||
resolve-dir "^1.0.1"
|
||||
|
||||
flat-cache@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
|
||||
integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==
|
||||
dependencies:
|
||||
flatted "^2.0.0"
|
||||
rimraf "2.6.3"
|
||||
write "1.0.3"
|
||||
|
||||
flatted@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
|
||||
integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
||||
|
|
@ -1695,6 +1889,11 @@ function-bind@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
|
||||
|
||||
functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
|
|
@ -1773,7 +1972,7 @@ global-prefix@^1.0.1:
|
|||
is-windows "^1.0.1"
|
||||
which "^1.2.14"
|
||||
|
||||
globals@^11.1.0:
|
||||
globals@^11.1.0, globals@^11.7.0:
|
||||
version "11.11.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
|
||||
integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==
|
||||
|
|
@ -1923,7 +2122,7 @@ https-browserify@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.4:
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
|
|
@ -1947,6 +2146,19 @@ ignore-walk@^3.0.1:
|
|||
dependencies:
|
||||
minimatch "^3.0.4"
|
||||
|
||||
ignore@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
import-fresh@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.0.0.tgz#a3d897f420cab0e671236897f75bc14b4885c390"
|
||||
integrity sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==
|
||||
dependencies:
|
||||
parent-module "^1.0.0"
|
||||
resolve-from "^4.0.0"
|
||||
|
||||
import-local@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
|
||||
|
|
@ -1988,6 +2200,25 @@ ini@^1.3.4, ini@~1.3.0:
|
|||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
||||
inquirer@^6.2.2:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.2.tgz#46941176f65c9eb20804627149b743a218f25406"
|
||||
integrity sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==
|
||||
dependencies:
|
||||
ansi-escapes "^3.2.0"
|
||||
chalk "^2.4.2"
|
||||
cli-cursor "^2.1.0"
|
||||
cli-width "^2.0.0"
|
||||
external-editor "^3.0.3"
|
||||
figures "^2.0.0"
|
||||
lodash "^4.17.11"
|
||||
mute-stream "0.0.7"
|
||||
run-async "^2.2.0"
|
||||
rxjs "^6.4.0"
|
||||
string-width "^2.1.0"
|
||||
strip-ansi "^5.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
interpret@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||
|
|
@ -2147,6 +2378,11 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
|||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-promise@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
||||
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
|
||||
|
||||
is-regex@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
|
||||
|
|
@ -2621,6 +2857,14 @@ js-yaml@^3.12.0:
|
|||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js-yaml@^3.13.0:
|
||||
version "3.13.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e"
|
||||
integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
|
|
@ -2678,6 +2922,11 @@ json-schema@0.2.3:
|
|||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
|
||||
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
|
|
@ -2753,7 +3002,7 @@ leven@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
|
||||
integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA=
|
||||
|
||||
levn@~0.3.0:
|
||||
levn@^0.3.0, levn@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
|
||||
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
|
||||
|
|
@ -2798,6 +3047,11 @@ lodash.sortby@^4.7.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash.unescape@4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
|
||||
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
|
||||
|
||||
lodash@^4.17.10, lodash@^4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
|
|
@ -3052,6 +3306,11 @@ msgpack5@^4.0.2:
|
|||
readable-stream "^2.3.6"
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
nan@^2.9.2:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
||||
|
|
@ -3287,6 +3546,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
|||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
onetime@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
|
||||
integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
|
|
@ -3295,7 +3561,7 @@ optimist@^0.6.1:
|
|||
minimist "~0.0.1"
|
||||
wordwrap "~0.0.2"
|
||||
|
||||
optionator@^0.8.1:
|
||||
optionator@^0.8.1, optionator@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
|
||||
integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
|
||||
|
|
@ -3333,7 +3599,7 @@ os-locale@^3.0.0:
|
|||
lcid "^2.0.0"
|
||||
mem "^4.0.0"
|
||||
|
||||
os-tmpdir@^1.0.0:
|
||||
os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||
|
|
@ -3406,6 +3672,13 @@ parallel-transform@^1.1.0:
|
|||
inherits "^2.0.3"
|
||||
readable-stream "^2.1.5"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
|
||||
dependencies:
|
||||
callsites "^3.0.0"
|
||||
|
||||
parse-asn1@^5.0.0:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc"
|
||||
|
|
@ -3461,6 +3734,11 @@ path-is-absolute@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
path-is-inside@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
|
||||
|
||||
path-key@^2.0.0, path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
|
|
@ -3546,6 +3824,11 @@ process@^0.11.10:
|
|||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
progress@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
promise-inflight@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
|
|
@ -3720,6 +4003,11 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
extend-shallow "^3.0.2"
|
||||
safe-regex "^1.1.0"
|
||||
|
||||
regexpp@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
|
||||
integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==
|
||||
|
||||
remove-trailing-separator@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
|
||||
|
|
@ -3787,6 +4075,11 @@ require-main-filename@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
|
||||
|
||||
requireindex@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
|
||||
integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
|
||||
|
||||
requires-port@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
|
|
@ -3812,6 +4105,11 @@ resolve-from@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
|
||||
integrity sha1-six699nWiBvItuZTM17rywoYh0g=
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
|
||||
|
||||
resolve-url@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
|
|
@ -3829,12 +4127,20 @@ resolve@1.x, resolve@^1.10.0, resolve@^1.3.2:
|
|||
dependencies:
|
||||
path-parse "^1.0.6"
|
||||
|
||||
restore-cursor@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
|
||||
integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
|
||||
dependencies:
|
||||
onetime "^2.0.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
ret@~0.1.10:
|
||||
version "0.1.15"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
|
||||
|
||||
rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
|
||||
rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
|
||||
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
|
||||
|
|
@ -3854,6 +4160,13 @@ rsvp@^3.3.3:
|
|||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
|
||||
integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==
|
||||
|
||||
run-async@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
|
||||
integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
|
||||
dependencies:
|
||||
is-promise "^2.1.0"
|
||||
|
||||
run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
|
||||
|
|
@ -3861,6 +4174,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
|||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rxjs@^6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
|
||||
integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
|
|
@ -3914,6 +4234,16 @@ schema-utils@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||
integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
|
||||
|
||||
semver@5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
|
||||
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
|
||||
|
||||
semver@^5.5.1:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
|
||||
integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==
|
||||
|
||||
serialize-javascript@^1.4.0:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.6.1.tgz#4d1f697ec49429a847ca6f442a2a755126c4d879"
|
||||
|
|
@ -3989,6 +4319,15 @@ slash@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
||||
|
||||
slice-ansi@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
|
||||
integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.0"
|
||||
astral-regex "^1.0.0"
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
|
||||
snapdragon-node@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
|
||||
|
|
@ -4185,7 +4524,7 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1:
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||
|
|
@ -4193,6 +4532,15 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string-width@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
|
||||
integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
|
||||
dependencies:
|
||||
emoji-regex "^7.0.1"
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
strip-ansi "^5.1.0"
|
||||
|
||||
string_decoder@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
|
||||
|
|
@ -4228,6 +4576,13 @@ strip-ansi@^5.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^4.0.0"
|
||||
|
||||
strip-ansi@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
|
||||
integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
|
||||
dependencies:
|
||||
ansi-regex "^4.1.0"
|
||||
|
||||
strip-bom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||
|
|
@ -4238,7 +4593,7 @@ strip-eof@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
|
@ -4262,6 +4617,16 @@ symbol-tree@^3.2.2:
|
|||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
|
||||
integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=
|
||||
|
||||
table@^5.2.3:
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-5.2.3.tgz#cde0cc6eb06751c009efab27e8c820ca5b67b7f2"
|
||||
integrity sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==
|
||||
dependencies:
|
||||
ajv "^6.9.1"
|
||||
lodash "^4.17.11"
|
||||
slice-ansi "^2.1.0"
|
||||
string-width "^3.0.0"
|
||||
|
||||
tapable@^1.0.0, tapable@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
|
||||
|
|
@ -4313,6 +4678,11 @@ test-exclude@^5.0.0:
|
|||
read-pkg-up "^4.0.0"
|
||||
require-main-filename "^1.0.1"
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||
|
||||
throat@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
|
||||
|
|
@ -4326,6 +4696,11 @@ through2@^2.0.0:
|
|||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through@^2.3.6:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
timers-browserify@^2.0.4:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
|
||||
|
|
@ -4333,6 +4708,13 @@ timers-browserify@^2.0.4:
|
|||
dependencies:
|
||||
setimmediate "^1.0.4"
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
|
||||
dependencies:
|
||||
os-tmpdir "~1.0.2"
|
||||
|
||||
tmpl@1.0.x:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
|
||||
|
|
@ -4427,11 +4809,18 @@ ts-loader@^4.4.1:
|
|||
micromatch "^3.1.4"
|
||||
semver "^5.0.1"
|
||||
|
||||
tslib@^1.9.0:
|
||||
tslib@^1.8.1, tslib@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
|
||||
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
|
||||
|
||||
tsutils@^3.7.0:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.9.1.tgz#2a40dc742943c71eca6d5c1994fcf999956be387"
|
||||
integrity sha512-hrxVtLtPqQr//p8/msPT1X1UYXUjizqSit5d9AQ5k38TcV38NyecL5xODNxa73cLe/5sdiJ+w1FqzDhRBA/anA==
|
||||
dependencies:
|
||||
tslib "^1.8.1"
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
|
@ -4461,10 +4850,10 @@ typedarray@^0.0.6:
|
|||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
|
||||
|
||||
typescript@^2.9.2:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
|
||||
integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==
|
||||
typescript@^3.4.0:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6"
|
||||
integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==
|
||||
|
||||
uglify-js@^3.1.4:
|
||||
version "3.4.9"
|
||||
|
|
@ -4784,6 +5173,13 @@ write-file-atomic@2.4.1:
|
|||
imurmurhash "^0.1.4"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
write@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3"
|
||||
integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==
|
||||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
ws@^5.2.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
|
||||
|
|
|
|||
|
|
@ -638,13 +638,21 @@ namespace Microsoft.AspNetCore.Components.Layouts
|
|||
}
|
||||
namespace Microsoft.AspNetCore.Components.Rendering
|
||||
{
|
||||
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||
public readonly partial struct ComponentRenderedText
|
||||
{
|
||||
private readonly object _dummy;
|
||||
private readonly int _dummyPrimitive;
|
||||
public int ComponentId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Collections.Generic.IEnumerable<string> Tokens { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
}
|
||||
public partial class HtmlRenderer : Microsoft.AspNetCore.Components.Rendering.Renderer
|
||||
{
|
||||
public HtmlRenderer(System.IServiceProvider serviceProvider, System.Func<string, string> htmlEncoder, Microsoft.AspNetCore.Components.Rendering.IDispatcher dispatcher) : base (default(System.IServiceProvider)) { }
|
||||
protected override void HandleException(System.Exception exception) { }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
|
||||
public System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> RenderComponentAsync<TComponent>(Microsoft.AspNetCore.Components.ParameterCollection initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync(System.Type componentType, Microsoft.AspNetCore.Components.ParameterCollection initialParameters) { throw null; }
|
||||
public System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Rendering.ComponentRenderedText> RenderComponentAsync<TComponent>(Microsoft.AspNetCore.Components.ParameterCollection initialParameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
protected override System.Threading.Tasks.Task UpdateDisplayAsync(in Microsoft.AspNetCore.Components.Rendering.RenderBatch renderBatch) { throw null; }
|
||||
}
|
||||
public partial interface IDispatcher
|
||||
|
|
@ -792,9 +800,10 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
{
|
||||
protected UriHelperBase() { }
|
||||
public event System.EventHandler<string> OnLocationChanged { add { } remove { } }
|
||||
protected virtual void EnsureInitialized() { }
|
||||
public string GetAbsoluteUri() { throw null; }
|
||||
public virtual string GetBaseUri() { throw null; }
|
||||
protected virtual void InitializeState() { }
|
||||
public virtual void InitializeState(string uriAbsolute, string baseUriAbsolute) { }
|
||||
public void NavigateTo(string uri) { }
|
||||
public void NavigateTo(string uri, bool forceLoad) { }
|
||||
protected abstract void NavigateToCore(string uri, bool forceLoad);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -47,21 +47,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <param name="componentType">The type of the <see cref="IComponent"/>.</param>
|
||||
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
|
||||
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
|
||||
public async Task<IEnumerable<string>> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
|
||||
public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterCollection initialParameters)
|
||||
{
|
||||
var frames = await CreateInitialRenderAsync(componentType, initialParameters);
|
||||
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
|
||||
|
||||
if (frames.Count == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new List<string>();
|
||||
var newPosition = RenderFrames(result, frames, 0, frames.Count);
|
||||
Debug.Assert(newPosition == frames.Count);
|
||||
return result;
|
||||
}
|
||||
var result = new List<string>();
|
||||
var newPosition = RenderFrames(result, frames, 0, frames.Count);
|
||||
Debug.Assert(newPosition == frames.Count);
|
||||
return new ComponentRenderedText(componentId, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
/// <typeparam name="TComponent">The type of the <see cref="IComponent"/>.</typeparam>
|
||||
/// <param name="initialParameters">A <see cref="ParameterCollection"/> with the initial parameters to render the component.</param>
|
||||
/// <returns>A <see cref="Task"/> that on completion returns a sequence of <see cref="string"/> fragments that represent the HTML text of the component.</returns>
|
||||
public Task<IEnumerable<string>> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
|
||||
public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterCollection initialParameters) where TComponent : IComponent
|
||||
{
|
||||
return RenderComponentAsync(typeof(TComponent), initialParameters);
|
||||
}
|
||||
|
|
@ -227,14 +220,14 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
return position + maxElements;
|
||||
}
|
||||
|
||||
private async Task<ArrayRange<RenderTreeFrame>> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
|
||||
private async Task<(int, ArrayRange<RenderTreeFrame>)> CreateInitialRenderAsync(Type componentType, ParameterCollection initialParameters)
|
||||
{
|
||||
var component = InstantiateComponent(componentType);
|
||||
var componentId = AssignRootComponentId(component);
|
||||
|
||||
await RenderRootComponentAsync(componentId, initialParameters);
|
||||
|
||||
return GetCurrentRenderTreeFrames(componentId);
|
||||
return (componentId, GetCurrentRenderTreeFrames(componentId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,6 +227,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
task = callback.InvokeAsync(eventArgs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
HandleException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isBatchInProgress = false;
|
||||
|
|
@ -336,7 +340,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
// The pendingTasks collection is only used during prerendering to track quiescence,
|
||||
// so will be null at other times.
|
||||
_pendingTasks?.Add(handledErrorTask);
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -442,7 +446,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
|
||||
// Fire off the execution of OnAfterRenderAsync, but don't wait for it
|
||||
// if there is async work to be done.
|
||||
_ = InvokeRenderCompletedCalls(batch.UpdatedComponents);
|
||||
_ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Ensure we catch errors while running the render functions of the components.
|
||||
HandleException(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -461,8 +470,34 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
}
|
||||
}
|
||||
|
||||
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents)
|
||||
private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents, Task updateDisplayTask)
|
||||
{
|
||||
if (updateDisplayTask.IsCanceled)
|
||||
{
|
||||
// The display update was cancelled (maybe due to a timeout on the components server-side case or due
|
||||
// to the renderer being disposed)
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
if (updateDisplayTask.IsFaulted)
|
||||
{
|
||||
// The display update failed so we don't care any more about running on render completed
|
||||
// fallbacks as the entire rendering process is going to be torn down.
|
||||
HandleException(updateDisplayTask.Exception);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!updateDisplayTask.IsCompleted)
|
||||
{
|
||||
var updatedComponentsId = new int[updatedComponents.Count];
|
||||
var updatedComponentsArray = updatedComponents.Array;
|
||||
for (int i = 0; i < updatedComponentsId.Length; i++)
|
||||
{
|
||||
updatedComponentsId[i] = updatedComponentsArray[i].ComponentId;
|
||||
}
|
||||
|
||||
return InvokeRenderCompletedCallsAfterUpdateDisplayTask(updateDisplayTask, updatedComponentsId);
|
||||
}
|
||||
|
||||
List<Task> batch = null;
|
||||
var array = updatedComponents.Array;
|
||||
for (var i = 0; i < updatedComponents.Count; i++)
|
||||
|
|
@ -470,36 +505,83 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
var componentState = GetOptionalComponentState(array[i].ComponentId);
|
||||
if (componentState != null)
|
||||
{
|
||||
// The component might be rendered and disposed in the same batch (if its parent
|
||||
// was rendered later in the batch, and removed the child from the tree).
|
||||
var task = componentState.NotifyRenderCompletedAsync();
|
||||
|
||||
// We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
|
||||
// unless we absolutely have to.
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
|
||||
{
|
||||
// Nothing to do here.
|
||||
continue;
|
||||
}
|
||||
else if (task.Status == TaskStatus.Faulted)
|
||||
{
|
||||
HandleException(task.Exception);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// The Task is incomplete.
|
||||
// Queue up the task and we can inspect it later.
|
||||
batch = batch ?? new List<Task>();
|
||||
batch.Add(GetErrorHandledTask(task));
|
||||
NotifyRenderCompleted(componentState, ref batch);
|
||||
}
|
||||
}
|
||||
|
||||
return batch != null ?
|
||||
Task.WhenAll(batch) :
|
||||
Task.CompletedTask;
|
||||
|
||||
}
|
||||
|
||||
private async Task InvokeRenderCompletedCallsAfterUpdateDisplayTask(
|
||||
Task updateDisplayTask,
|
||||
int[] updatedComponents)
|
||||
{
|
||||
try
|
||||
{
|
||||
await updateDisplayTask;
|
||||
}
|
||||
catch when (updateDisplayTask.IsCanceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch when (updateDisplayTask.IsFaulted)
|
||||
{
|
||||
HandleException(updateDisplayTask.Exception);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Task> batch = null;
|
||||
var array = updatedComponents;
|
||||
for (var i = 0; i < updatedComponents.Length; i++)
|
||||
{
|
||||
var componentState = GetOptionalComponentState(array[i]);
|
||||
if (componentState != null)
|
||||
{
|
||||
NotifyRenderCompleted(componentState, ref batch);
|
||||
}
|
||||
}
|
||||
|
||||
var result = batch != null ?
|
||||
Task.WhenAll(batch) :
|
||||
Task.CompletedTask;
|
||||
|
||||
await result;
|
||||
}
|
||||
|
||||
private void NotifyRenderCompleted(ComponentState state, ref List<Task> batch)
|
||||
{
|
||||
// The component might be rendered and disposed in the same batch (if its parent
|
||||
// was rendered later in the batch, and removed the child from the tree).
|
||||
// This can also happen between batches if the UI takes some time to update and within
|
||||
// that time the component gets removed out of the tree because the parent chose not to
|
||||
// render it in a later batch.
|
||||
// In any of the two cases mentioned happens, OnAfterRenderAsync won't run but that is
|
||||
// ok.
|
||||
var task = state.NotifyRenderCompletedAsync();
|
||||
|
||||
// We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
|
||||
// unless we absolutely have to.
|
||||
if (task.IsCompleted)
|
||||
{
|
||||
if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
|
||||
{
|
||||
// Nothing to do here.
|
||||
return;
|
||||
}
|
||||
else if (task.Status == TaskStatus.Faulted)
|
||||
{
|
||||
HandleException(task.Exception);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The Task is incomplete.
|
||||
// Queue up the task and we can inspect it later.
|
||||
batch = batch ?? new List<Task>();
|
||||
batch.Add(GetErrorHandledTask(task));
|
||||
}
|
||||
|
||||
private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
{
|
||||
add
|
||||
{
|
||||
EnsureInitialized();
|
||||
AssertInitialized();
|
||||
_onLocationChanged += value;
|
||||
}
|
||||
remove
|
||||
|
|
@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
/// <param name="forceLoad">If true, bypasses client-side routing and forces the browser to load the new page from the server, whether or not the URI would normally be handled by the client-side router.</param>
|
||||
public void NavigateTo(string uri, bool forceLoad)
|
||||
{
|
||||
EnsureInitialized();
|
||||
AssertInitialized();
|
||||
NavigateToCore(uri, forceLoad);
|
||||
}
|
||||
|
||||
|
|
@ -70,10 +70,35 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
protected abstract void NavigateToCore(string uri, bool forceLoad);
|
||||
|
||||
/// <summary>
|
||||
/// Called to initialize BaseURI and current URI before those values the first time.
|
||||
/// Override this method to dynamically calculate the those values.
|
||||
/// Called to initialize BaseURI and current URI before these values are used for the first time.
|
||||
/// Override this method to dynamically calculate these values.
|
||||
/// </summary>
|
||||
protected virtual void InitializeState()
|
||||
public virtual void InitializeState(string uriAbsolute, string baseUriAbsolute)
|
||||
{
|
||||
if (uriAbsolute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(uriAbsolute));
|
||||
}
|
||||
|
||||
if (baseUriAbsolute == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(baseUriAbsolute));
|
||||
}
|
||||
|
||||
if (_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException($"'{typeof(UriHelperBase).Name}' already initialized.");
|
||||
}
|
||||
_isInitialized = true;
|
||||
|
||||
SetAbsoluteUri(uriAbsolute);
|
||||
SetAbsoluteBaseUri(baseUriAbsolute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows derived classes to lazyly self initialize. It does nothing unless overriden.
|
||||
/// </summary>
|
||||
protected virtual void EnsureInitialized()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +108,7 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
/// <returns>The current absolute URI.</returns>
|
||||
public string GetAbsoluteUri()
|
||||
{
|
||||
EnsureInitialized();
|
||||
AssertInitialized();
|
||||
return _uri;
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
/// <returns>The URI prefix, which has a trailing slash.</returns>
|
||||
public virtual string GetBaseUri()
|
||||
{
|
||||
EnsureInitialized();
|
||||
AssertInitialized();
|
||||
return _baseUriString;
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +132,7 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
/// <returns>The absolute URI.</returns>
|
||||
public Uri ToAbsoluteUri(string href)
|
||||
{
|
||||
EnsureInitialized();
|
||||
AssertInitialized();
|
||||
return new Uri(_baseUri, href);
|
||||
}
|
||||
|
||||
|
|
@ -185,12 +210,16 @@ namespace Microsoft.AspNetCore.Components.Services
|
|||
_onLocationChanged?.Invoke(this, _uri);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
private void AssertInitialized()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
InitializeState();
|
||||
_isInitialized = true;
|
||||
EnsureInitialized();
|
||||
}
|
||||
|
||||
if (!_isInitialized)
|
||||
{
|
||||
throw new InvalidOperationException($"'{GetType().Name}' has not been initialized.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2454,6 +2454,50 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
Assert.Equal(2, component.OnAfterRenderCallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingAsynchronously()
|
||||
{
|
||||
// Arrange
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
var afterRenderTcs = new TaskCompletionSource<object>();
|
||||
var onAfterRenderCallCountLog = new List<int>();
|
||||
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task);
|
||||
var renderer = new AsyncUpdateTestRenderer()
|
||||
{
|
||||
OnUpdateDisplayAsync = _ => tcs.Task
|
||||
};
|
||||
renderer.AssignRootComponentId(component);
|
||||
|
||||
// Act
|
||||
component.TriggerRender();
|
||||
tcs.SetResult(null);
|
||||
afterRenderTcs.SetResult(null);
|
||||
|
||||
// Assert
|
||||
Assert.True(component.Called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CallsAfterRenderAfterTheUIHasFinishedUpdatingSynchronously()
|
||||
{
|
||||
// Arrange
|
||||
var afterRenderTcs = new TaskCompletionSource<object>();
|
||||
var onAfterRenderCallCountLog = new List<int>();
|
||||
var component = new AsyncAfterRenderComponent(afterRenderTcs.Task);
|
||||
var renderer = new AsyncUpdateTestRenderer()
|
||||
{
|
||||
OnUpdateDisplayAsync = _ => Task.CompletedTask
|
||||
};
|
||||
renderer.AssignRootComponentId(component);
|
||||
|
||||
// Act
|
||||
component.TriggerRender();
|
||||
afterRenderTcs.SetResult(null);
|
||||
|
||||
// Assert
|
||||
Assert.True(component.Called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotCallOnAfterRenderForComponentsNotRendered()
|
||||
{
|
||||
|
|
@ -3763,5 +3807,39 @@ namespace Microsoft.AspNetCore.Components.Test
|
|||
await TaskToAwait;
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncUpdateTestRenderer : TestRenderer
|
||||
{
|
||||
public Func<RenderBatch, Task> OnUpdateDisplayAsync { get; set; }
|
||||
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
|
||||
{
|
||||
return OnUpdateDisplayAsync(renderBatch);
|
||||
}
|
||||
}
|
||||
|
||||
private class AsyncAfterRenderComponent : AutoRenderComponent, IHandleAfterRender
|
||||
{
|
||||
private readonly Task _task;
|
||||
|
||||
public AsyncAfterRenderComponent(Task task)
|
||||
{
|
||||
_task = task;
|
||||
}
|
||||
|
||||
public bool Called { get; private set; }
|
||||
|
||||
public async Task OnAfterRenderAsync()
|
||||
{
|
||||
await _task;
|
||||
Called = true;
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenElement(0, "p");
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,12 +390,12 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
Assert.Equal(expectedHtml, result);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetResult(Task<IEnumerable<string>> task)
|
||||
private IEnumerable<string> GetResult(Task<ComponentRenderedText> task)
|
||||
{
|
||||
Assert.True(task.IsCompleted);
|
||||
if (task.IsCompletedSuccessfully)
|
||||
{
|
||||
return task.Result;
|
||||
return task.Result.Tokens;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -440,7 +440,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
Assert.Equal(expectedHtml, result.Tokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
|||
})));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedHtml, result);
|
||||
Assert.Equal(expectedHtml, result.Tokens);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -432,6 +432,10 @@ namespace Microsoft.AspNetCore.Components.Server.BlazorPack
|
|||
writer.Write(byteArray);
|
||||
break;
|
||||
|
||||
case Exception exception:
|
||||
writer.Write(exception.ToString());
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new FormatException($"Unsupported argument type {argument.GetType()}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,22 @@ namespace Microsoft.AspNetCore.Builder
|
|||
/// </summary>
|
||||
public static class ComponentEndpointRouteBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps the SignalR <see cref="ComponentHub"/> to the path <paramref name="path"/> and associates
|
||||
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
|
||||
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
|
||||
public static IEndpointConventionBuilder MapComponentHub(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
if (endpoints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(endpoints));
|
||||
}
|
||||
|
||||
return endpoints.MapHub<ComponentHub>(ComponentHub.DefaultPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps the SignalR <see cref="ComponentHub"/> to the path <paramref name="path"/> and associates
|
||||
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
internal class CircuitClientProxy : IClientProxy
|
||||
{
|
||||
public static readonly CircuitClientProxy OfflineClient = new CircuitClientProxy();
|
||||
|
||||
private CircuitClientProxy()
|
||||
public CircuitClientProxy()
|
||||
{
|
||||
Connected = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Microsoft.AspNetCore.Components.Browser;
|
||||
using Microsoft.AspNetCore.Components.Browser.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
|
|
@ -95,15 +96,42 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
public IDispatcher Dispatcher { get; }
|
||||
|
||||
public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
|
||||
public Task<ComponentRenderedText> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
|
||||
{
|
||||
return Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
var result = await Renderer.RenderComponentAsync(componentType, parameters);
|
||||
|
||||
// When we prerender we start the circuit in a disconnected state. As such, we only call
|
||||
// OnCircuitOpenenedAsync here and when the client reconnects we run OnConnectionUpAsync
|
||||
await OnCircuitOpenedAsync(CancellationToken.None);
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException)
|
||||
{
|
||||
if (!_initialized)
|
||||
{
|
||||
_initialized = true;
|
||||
UnhandledException += unhandledException;
|
||||
var uriHelper = (RemoteUriHelper)Services.GetRequiredService<IUriHelper>();
|
||||
if (!uriHelper.HasAttachedJSRuntime)
|
||||
{
|
||||
uriHelper.AttachJsRuntime(JSRuntime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void SendPendingBatches()
|
||||
{
|
||||
// Dispatch any buffered renders we accumulated during a disconnect.
|
||||
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
|
||||
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
|
||||
var _ = Renderer.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Renderer.InvokeAsync(async () =>
|
||||
|
|
@ -122,8 +150,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
// processing incoming JSInterop calls or similar.
|
||||
for (var i = 0; i < Descriptors.Count; i++)
|
||||
{
|
||||
var (componentType, domElementSelector) = Descriptors[i];
|
||||
await Renderer.AddComponentAsync(componentType, domElementSelector);
|
||||
var (componentType, domElementSelector, prerendered) = Descriptors[i];
|
||||
if (!prerendered)
|
||||
{
|
||||
await Renderer.AddComponentAsync(componentType, domElementSelector);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -248,10 +279,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
await OnConnectionDownAsync(CancellationToken.None);
|
||||
await OnCircuitDownAsync();
|
||||
Renderer.Dispose();
|
||||
_scope.Dispose();
|
||||
}));
|
||||
|
||||
_scope.Dispose();
|
||||
Renderer.Dispose();
|
||||
}
|
||||
|
||||
private void AssertInitialized()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -12,37 +12,66 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
internal class CircuitPrerenderer : IComponentPrerenderer
|
||||
{
|
||||
private readonly CircuitFactory _circuitFactory;
|
||||
private static object CircuitHostKey = new object();
|
||||
|
||||
public CircuitPrerenderer(CircuitFactory circuitFactory)
|
||||
private readonly CircuitFactory _circuitFactory;
|
||||
private readonly CircuitRegistry _registry;
|
||||
|
||||
public CircuitPrerenderer(CircuitFactory circuitFactory, CircuitRegistry registry)
|
||||
{
|
||||
_circuitFactory = circuitFactory;
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
||||
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
||||
{
|
||||
var context = prerenderingContext.Context;
|
||||
var circuitHost = _circuitFactory.CreateCircuitHost(
|
||||
context,
|
||||
client: CircuitClientProxy.OfflineClient,
|
||||
GetFullUri(context.Request),
|
||||
GetFullBaseUri(context.Request));
|
||||
var circuitHost = GetOrCreateCircuitHost(context);
|
||||
|
||||
// We don't need to unsubscribe because the circuit host object is scoped to this call.
|
||||
circuitHost.UnhandledException += CircuitHost_UnhandledException;
|
||||
var renderResult = await circuitHost.PrerenderComponentAsync(
|
||||
prerenderingContext.ComponentType,
|
||||
prerenderingContext.Parameters);
|
||||
|
||||
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
|
||||
// reconnect to it from the ComponentsHub. If we keep the circuit/renderer we also need to unsubscribe this error
|
||||
// handler.
|
||||
try
|
||||
circuitHost.Descriptors.Add(new ComponentDescriptor
|
||||
{
|
||||
return await circuitHost.PrerenderComponentAsync(
|
||||
prerenderingContext.ComponentType,
|
||||
prerenderingContext.Parameters);
|
||||
ComponentType = prerenderingContext.ComponentType,
|
||||
Prerendered = true
|
||||
});
|
||||
|
||||
var result = new[] {
|
||||
$"<!-- M.A.C.Component:{{\"circuitId\":\"{circuitHost.CircuitId}\",\"rendererId\":\"{circuitHost.Renderer.Id}\",\"componentId\":\"{renderResult.ComponentId}\"}} -->",
|
||||
}.Concat(renderResult.Tokens).Concat(
|
||||
new[] {
|
||||
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
|
||||
});
|
||||
|
||||
return new ComponentPrerenderResult(result);
|
||||
}
|
||||
|
||||
private CircuitHost GetOrCreateCircuitHost(HttpContext context)
|
||||
{
|
||||
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
|
||||
{
|
||||
return (CircuitHost)existingHost;
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
await circuitHost.DisposeAsync();
|
||||
var result = _circuitFactory.CreateCircuitHost(
|
||||
context,
|
||||
client: new CircuitClientProxy(), // This creates an "offline" client.
|
||||
GetFullUri(context.Request),
|
||||
GetFullBaseUri(context.Request));
|
||||
|
||||
result.UnhandledException += CircuitHost_UnhandledException;
|
||||
context.Response.OnCompleted(() =>
|
||||
{
|
||||
result.UnhandledException -= CircuitHost_UnhandledException;
|
||||
_registry.RegisterDisconnectedCircuit(result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
context.Items.Add(CircuitHostKey, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Debug.Assert(result, "This operation operates inside of a lock. We expect the previously inspected value to be still here.");
|
||||
|
||||
circuitHost.Client.SetDisconnected();
|
||||
RegisterDisconnectedCircuit(circuitHost);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RegisterDisconnectedCircuit(CircuitHost circuitHost)
|
||||
{
|
||||
var entryOptions = new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_options.DisconnectedCircuitRetentionPeriod),
|
||||
|
|
@ -129,7 +135,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
};
|
||||
|
||||
DisconnectedCircuits.Set(circuitHost.CircuitId, circuitHost, entryOptions);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual async Task<CircuitHost> ConnectAsync(string circuitId, IClientProxy clientProxy, string connectionId, CancellationToken cancellationToken)
|
||||
|
|
|
|||
|
|
@ -46,13 +46,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
componentContext.Initialize(client);
|
||||
|
||||
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
|
||||
if (client != CircuitClientProxy.OfflineClient)
|
||||
if (client.Connected)
|
||||
{
|
||||
uriHelper.Initialize(uriAbsolute, baseUriAbsolute, jsRuntime);
|
||||
uriHelper.AttachJsRuntime(jsRuntime);
|
||||
uriHelper.InitializeState(
|
||||
uriAbsolute,
|
||||
baseUriAbsolute);
|
||||
}
|
||||
else
|
||||
{
|
||||
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
|
||||
uriHelper.InitializeState(uriAbsolute, baseUriAbsolute);
|
||||
}
|
||||
|
||||
var rendererRegistry = new RendererRegistry();
|
||||
|
|
@ -87,12 +90,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
return circuitHost;
|
||||
}
|
||||
|
||||
private static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
|
||||
internal static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
|
||||
{
|
||||
if (client == CircuitClientProxy.OfflineClient)
|
||||
if (!client.Connected)
|
||||
{
|
||||
// This is the prerendering case.
|
||||
return Array.Empty<ComponentDescriptor>();
|
||||
// This is the prerendering case. Descriptors will be registered by the prerenderer.
|
||||
return new List<ComponentDescriptor>();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -106,10 +109,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
}
|
||||
|
||||
var componentsMetadata = endpoint.Metadata.OfType<ComponentDescriptor>().ToList();
|
||||
if (componentsMetadata.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("No component was registered with the component hub.");
|
||||
}
|
||||
|
||||
return componentsMetadata;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
{
|
||||
internal class RemoteJSRuntime : JSRuntimeBase
|
||||
{
|
||||
private IClientProxy _clientProxy;
|
||||
private CircuitClientProxy _clientProxy;
|
||||
|
||||
internal void Initialize(CircuitClientProxy clientProxy)
|
||||
{
|
||||
|
|
@ -18,11 +18,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
if (_clientProxy == CircuitClientProxy.OfflineClient)
|
||||
if (!_clientProxy.Connected)
|
||||
{
|
||||
var errorMessage = "JavaScript interop calls cannot be issued while the client is not connected, because the server is not able to interop with the browser at this time. " +
|
||||
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during periods where the client is not connected.";
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being " +
|
||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
||||
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
|
||||
"attempted during prerendering or while the client is disconnected.");
|
||||
}
|
||||
|
||||
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -19,18 +20,12 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
{
|
||||
internal class RemoteRenderer : HtmlRenderer
|
||||
{
|
||||
// The purpose of the timeout is just to ensure server resources are released at some
|
||||
// point if the client disconnects without sending back an ACK after a render
|
||||
private const int TimeoutMilliseconds = 60 * 1000;
|
||||
|
||||
private readonly int _id;
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private readonly CircuitClientProxy _client;
|
||||
private readonly RendererRegistry _rendererRegistry;
|
||||
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
|
||||
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
|
||||
private readonly ILogger _logger;
|
||||
private long _nextRenderId = 1;
|
||||
private bool _disposing = false;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies when a rendering exception occured.
|
||||
|
|
@ -54,11 +49,13 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
_jsRuntime = jsRuntime;
|
||||
_client = client;
|
||||
|
||||
_id = _rendererRegistry.Add(this);
|
||||
Id = _rendererRegistry.Add(this);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal ConcurrentQueue<byte[]> OfflineRenderBatches = new ConcurrentQueue<byte[]>();
|
||||
internal ConcurrentQueue<PendingRender> PendingRenderBatches = new ConcurrentQueue<PendingRender>();
|
||||
|
||||
public int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
|
||||
|
|
@ -73,7 +70,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
|
||||
var attachComponentTask = _jsRuntime.InvokeAsync<object>(
|
||||
"Blazor._internal.attachRootComponentToElement",
|
||||
_id,
|
||||
Id,
|
||||
domElementSelector,
|
||||
componentId);
|
||||
CaptureAsyncExceptions(attachComponentTask);
|
||||
|
|
@ -102,13 +99,24 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_disposing = true;
|
||||
base.Dispose(true);
|
||||
_rendererRegistry.TryRemove(_id);
|
||||
while (PendingRenderBatches.TryDequeue(out var entry))
|
||||
{
|
||||
entry.CompletionSource.TrySetCanceled();
|
||||
}
|
||||
_rendererRegistry.TryRemove(Id);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
||||
{
|
||||
if (_disposing)
|
||||
{
|
||||
// We are being disposed, so do no work.
|
||||
return Task.FromCanceled<object>(CancellationToken.None);
|
||||
}
|
||||
|
||||
// Note that we have to capture the data as a byte[] synchronously here, because
|
||||
// SignalR's SendAsync can wait an arbitrary duration before serializing the params.
|
||||
// The RenderBatch buffer will get reused by subsequent renders, so we need to
|
||||
|
|
@ -126,77 +134,112 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
batchBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
if (!_client.Connected)
|
||||
{
|
||||
// Buffer the rendered batches while the client is disconnected. We'll send it down once the client reconnects.
|
||||
OfflineRenderBatches.Enqueue(batchBytes);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId);
|
||||
return WriteBatchBytes(batchBytes);
|
||||
}
|
||||
|
||||
public async Task ProcessBufferedRenderBatches()
|
||||
{
|
||||
// The server may discover that the client disconnected while we're attempting to write empty rendered batches.
|
||||
// Discontinue writing in this event.
|
||||
while (_client.Connected && OfflineRenderBatches.TryDequeue(out var renderBatch))
|
||||
{
|
||||
await WriteBatchBytes(renderBatch);
|
||||
}
|
||||
}
|
||||
|
||||
private Task WriteBatchBytes(byte[] batchBytes)
|
||||
{
|
||||
var renderId = Interlocked.Increment(ref _nextRenderId);
|
||||
|
||||
var pendingRenderInfo = new AutoCancelTaskCompletionSource<object>(TimeoutMilliseconds);
|
||||
_pendingRenders[renderId] = pendingRenderInfo;
|
||||
var pendingRender = new PendingRender(
|
||||
renderId,
|
||||
batchBytes,
|
||||
new TaskCompletionSource<object>());
|
||||
|
||||
// Send the render batch to the client
|
||||
// If the "send" operation fails (synchronously or asynchronously), abort
|
||||
// the whole render with that exception
|
||||
try
|
||||
{
|
||||
_client.SendAsync("JS.RenderBatch", _id, renderId, batchBytes).ContinueWith(sendTask =>
|
||||
{
|
||||
if (sendTask.IsFaulted)
|
||||
{
|
||||
pendingRenderInfo.TrySetException(sendTask.Exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception syncException)
|
||||
{
|
||||
pendingRenderInfo.TrySetException(syncException);
|
||||
}
|
||||
// Buffer the rendered batches no matter what. We'll send it down immediately when the client
|
||||
// is connected or right after the client reconnects.
|
||||
|
||||
// When the render is completed (success, fail, or timeout), stop tracking it
|
||||
return pendingRenderInfo.Task.ContinueWith(task =>
|
||||
{
|
||||
_pendingRenders.TryRemove(renderId, out var ignored);
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
UnhandledException?.Invoke(this, task.Exception);
|
||||
}
|
||||
});
|
||||
PendingRenderBatches.Enqueue(pendingRender);
|
||||
|
||||
// Fire and forget the initial send for this batch (if connected). Otherwise it will be sent
|
||||
// as soon as the client reconnects.
|
||||
var _ = WriteBatchBytesAsync(pendingRender);
|
||||
|
||||
return pendingRender.CompletionSource.Task;
|
||||
}
|
||||
|
||||
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
|
||||
public Task ProcessBufferedRenderBatches()
|
||||
{
|
||||
if (_pendingRenders.TryGetValue(renderId, out var pendingRenderInfo))
|
||||
// All the batches are sent in order based on the fact that SignalR
|
||||
// provides ordering for the underlying messages and that the batches
|
||||
// are always in order.
|
||||
return Task.WhenAll(PendingRenderBatches.Select(b => WriteBatchBytesAsync(b)));
|
||||
}
|
||||
|
||||
private async Task WriteBatchBytesAsync(PendingRender pending)
|
||||
{
|
||||
// Send the render batch to the client
|
||||
// If the "send" operation fails (synchronously or asynchronously) or the client
|
||||
// gets disconected simply give up. This likely means that
|
||||
// the circuit went offline while sending the data, so simply wait until the
|
||||
// client reconnects back or the circuit gets evicted because it stayed
|
||||
// disconnected for too long.
|
||||
|
||||
try
|
||||
{
|
||||
if (errorMessageOrNull == null)
|
||||
if (!_client.Connected)
|
||||
{
|
||||
pendingRenderInfo.TrySetResult(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingRenderInfo.TrySetException(
|
||||
new RemoteRendererException(errorMessageOrNull));
|
||||
// If we detect that the client is offline. Simply stop trying to send the payload.
|
||||
// When the client reconnects we'll resend it.
|
||||
return;
|
||||
}
|
||||
|
||||
Log.BeginUpdateDisplayAsync(_logger, _client.ConnectionId);
|
||||
await _client.SendAsync("JS.RenderBatch", Id, pending.BatchId, pending.Data);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.SendBatchDataFailed(_logger, e);
|
||||
}
|
||||
|
||||
// We don't have to remove the entry from the list of pending batches if we fail to send it or the client fails to
|
||||
// acknowledge that it received it. We simply keep it in the queue until we receive another ack from the client for
|
||||
// a later batch (clientBatchId > thisBatchId) or the circuit becomes disconnected and we ultimately get evicted and
|
||||
// disposed.
|
||||
}
|
||||
|
||||
public void OnRenderCompleted(long incomingBatchId, string errorMessageOrNull)
|
||||
{
|
||||
if (_disposing)
|
||||
{
|
||||
// Disposing so don't do work.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PendingRenderBatches.TryDequeue(out var entry) || entry.BatchId != incomingBatchId)
|
||||
{
|
||||
HandleException(
|
||||
new InvalidOperationException($"Received a notification for a rendered batch when not expecting it. Batch id '{incomingBatchId}'."));
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = $"Completing batch {entry.BatchId} " +
|
||||
errorMessageOrNull == null ? "without error." : "with error.";
|
||||
|
||||
_logger.LogDebug(message);
|
||||
CompleteRender(entry.CompletionSource, errorMessageOrNull);
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteRender(TaskCompletionSource<object> pendingRenderInfo, string errorMessageOrNull)
|
||||
{
|
||||
if (errorMessageOrNull == null)
|
||||
{
|
||||
pendingRenderInfo.TrySetResult(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingRenderInfo.TrySetException(new RemoteRendererException(errorMessageOrNull));
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct PendingRender
|
||||
{
|
||||
public PendingRender(long batchId, byte[] data, TaskCompletionSource<object> completionSource)
|
||||
{
|
||||
BatchId = batchId;
|
||||
Data = data;
|
||||
CompletionSource = completionSource;
|
||||
}
|
||||
|
||||
public long BatchId { get; }
|
||||
public byte[] Data { get; }
|
||||
public TaskCompletionSource<object> CompletionSource { get; }
|
||||
}
|
||||
|
||||
private void CaptureAsyncExceptions(Task task)
|
||||
|
|
@ -215,12 +258,14 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
|
||||
private static readonly Action<ILogger, string, Exception> _beginUpdateDisplayAsync;
|
||||
private static readonly Action<ILogger, string, Exception> _bufferingRenderDisconnectedClient;
|
||||
private static readonly Action<ILogger, string, Exception> _sendBatchDataFailed;
|
||||
|
||||
private static class EventIds
|
||||
{
|
||||
public static readonly EventId UnhandledExceptionRenderingComponent = new EventId(100, "ExceptionRenderingComponent");
|
||||
public static readonly EventId BeginUpdateDisplayAsync = new EventId(101, "BeginUpdateDisplayAsync");
|
||||
public static readonly EventId SkipUpdateDisplayAsync = new EventId(102, "SkipUpdateDisplayAsync");
|
||||
public static readonly EventId SendBatchDataFailed = new EventId(103, "SendBatchDataFailed");
|
||||
}
|
||||
|
||||
static Log()
|
||||
|
|
@ -239,6 +284,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
LogLevel.Trace,
|
||||
EventIds.SkipUpdateDisplayAsync,
|
||||
"Buffering remote render because the client on connection {ConnectionId} is disconnected.");
|
||||
|
||||
_sendBatchDataFailed = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
EventIds.SendBatchDataFailed,
|
||||
"Sending data for batch failed: {Message}");
|
||||
}
|
||||
|
||||
public static void SendBatchDataFailed(ILogger logger, Exception exception)
|
||||
{
|
||||
_sendBatchDataFailed(logger, exception.Message, exception);
|
||||
}
|
||||
|
||||
public static void UnhandledExceptionRenderingComponent(ILogger logger, Exception exception)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.JSInterop;
|
||||
using Interop = Microsoft.AspNetCore.Components.Browser.BrowserUriHelperInterop;
|
||||
|
||||
|
|
@ -15,6 +16,14 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
public class RemoteUriHelper : UriHelperBase
|
||||
{
|
||||
private IJSRuntime _jsRuntime;
|
||||
private readonly ILogger<RemoteUriHelper> _logger;
|
||||
|
||||
public RemoteUriHelper(ILogger<RemoteUriHelper> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool HasAttachedJSRuntime => _jsRuntime != null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the <see cref="RemoteUriHelper"/>.
|
||||
|
|
@ -22,10 +31,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
|
||||
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
|
||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
|
||||
public void Initialize(string uriAbsolute, string baseUriAbsolute)
|
||||
public override void InitializeState(string uriAbsolute, string baseUriAbsolute)
|
||||
{
|
||||
SetAbsoluteBaseUri(baseUriAbsolute);
|
||||
SetAbsoluteUri(uriAbsolute);
|
||||
base.InitializeState(uriAbsolute, baseUriAbsolute);
|
||||
TriggerOnLocationChanged();
|
||||
}
|
||||
|
||||
|
|
@ -35,23 +43,20 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
|
||||
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
|
||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
|
||||
public void Initialize(string uriAbsolute, string baseUriAbsolute, IJSRuntime jsRuntime)
|
||||
internal void AttachJsRuntime(IJSRuntime jsRuntime)
|
||||
{
|
||||
if (_jsRuntime != null)
|
||||
{
|
||||
throw new InvalidOperationException("JavaScript runtime already initialized.");
|
||||
}
|
||||
|
||||
_jsRuntime = jsRuntime;
|
||||
|
||||
Initialize(uriAbsolute, baseUriAbsolute);
|
||||
|
||||
_jsRuntime.InvokeAsync<object>(
|
||||
Interop.EnableNavigationInterception,
|
||||
typeof(RemoteUriHelper).Assembly.GetName().Name,
|
||||
nameof(NotifyLocationChanged));
|
||||
}
|
||||
|
||||
_logger.LogInformation($"{nameof(RemoteUriHelper)} initialized.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For framework use only.
|
||||
|
|
@ -69,14 +74,21 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
var uriHelper = (RemoteUriHelper)circuit.Services.GetRequiredService<IUriHelper>();
|
||||
|
||||
uriHelper.SetAbsoluteUri(uriAbsolute);
|
||||
|
||||
uriHelper._logger.LogDebug($"Location changed to '{uriAbsolute}'.");
|
||||
uriHelper.TriggerOnLocationChanged();
|
||||
}
|
||||
|
||||
protected override void NavigateToCore(string uri, bool forceLoad)
|
||||
{
|
||||
_logger.LogDebug($"Log debug {uri} force load {forceLoad}.");
|
||||
|
||||
if (_jsRuntime == null)
|
||||
{
|
||||
throw new InvalidOperationException("Navigation is not allowed during prerendering.");
|
||||
throw new InvalidOperationException("Navigation commands can not be issued at this time. This is because the component is being " +
|
||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
||||
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
|
||||
"attempted during prerendering or while the client is disconnected.");
|
||||
}
|
||||
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -67,6 +69,16 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
|
||||
{
|
||||
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
|
||||
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext(), circuitClient).Count == 0)
|
||||
{
|
||||
var endpointFeature = Context.GetHttpContext().Features.Get<IEndpointFeature>();
|
||||
var endpoint = endpointFeature?.Endpoint;
|
||||
|
||||
_logger.LogInformation($"No components registered in the current endpoint '{endpoint.DisplayName}'.");
|
||||
|
||||
// No components preregistered so return. This is totally normal if the components were prerendered.
|
||||
return null;
|
||||
}
|
||||
|
||||
var circuitHost = _circuitFactory.CreateCircuitHost(
|
||||
Context.GetHttpContext(),
|
||||
|
|
@ -99,10 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
{
|
||||
CircuitHost = circuitHost;
|
||||
|
||||
// Dispatch any buffered renders we accumulated during a disconnect.
|
||||
// Note that while the rendering is async, we cannot await it here. The Task returned by ProcessBufferedRenderBatches relies on
|
||||
// OnRenderCompleted to be invoked to complete, and SignalR does not allow concurrent hub method invocations.
|
||||
_ = circuitHost.Renderer.ProcessBufferedRenderBatches();
|
||||
circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException);
|
||||
circuitHost.SendPendingBatches();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +132,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
/// </summary>
|
||||
public void OnRenderCompleted(long renderId, string errorMessageOrNull)
|
||||
{
|
||||
_logger.LogInformation($"Received confirmation for batch {renderId}.");
|
||||
EnsureCircuitHost().Renderer.OnRenderCompleted(renderId, errorMessageOrNull);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
|
||||
public string Selector { get; set; }
|
||||
|
||||
public void Deconstruct(out Type componentType, out string selector)
|
||||
public bool Prerendered { get; set; }
|
||||
|
||||
public void Deconstruct(out Type componentType, out string selector, out bool prerendered)
|
||||
{
|
||||
componentType = ComponentType;
|
||||
selector = Selector;
|
||||
prerendered = Prerendered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
|
||||
<Reference Include="Microsoft.Extensions.Logging" />
|
||||
<Reference Include="Microsoft.AspNetCore.SignalR" />
|
||||
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
|
||||
<Reference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
|
|
@ -16,6 +15,6 @@ namespace Microsoft.AspNetCore.Components.Server
|
|||
/// </summary>
|
||||
/// <param name="context">The context in which the prerrendering is happening.</param>
|
||||
/// <returns><see cref="Task{TResult}"/> that will complete when the prerendering is done.</returns>
|
||||
Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context);
|
||||
Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,36 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Assert.True(remoteRenderer.Disposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisposeAsync_DisposesRendererWithinSynchronizationContext()
|
||||
{
|
||||
// Arrange
|
||||
var serviceScope = new Mock<IServiceScope>();
|
||||
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
|
||||
var circuitHost = TestCircuitHost.Create(
|
||||
serviceScope.Object,
|
||||
remoteRenderer);
|
||||
|
||||
var component = new DispatcherComponent(circuitHost.Dispatcher);
|
||||
circuitHost.Renderer.AssignRootComponentId(component);
|
||||
var original = SynchronizationContext.Current;
|
||||
SynchronizationContext.SetSynchronizationContext(null);
|
||||
|
||||
// Act & Assert
|
||||
try
|
||||
{
|
||||
Assert.Null(SynchronizationContext.Current);
|
||||
await circuitHost.DisposeAsync();
|
||||
Assert.True(component.Called);
|
||||
Assert.Null(SynchronizationContext.Current);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Not sure if the line above messes up the xunit sync context, so just being cautious here.
|
||||
SynchronizationContext.SetSynchronizationContext(original);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitializeAsync_InvokesHandlers()
|
||||
{
|
||||
|
|
@ -190,5 +220,22 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
Disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private class DispatcherComponent : ComponentBase, IDisposable
|
||||
{
|
||||
public DispatcherComponent(IDispatcher dispatcher)
|
||||
{
|
||||
Dispatcher = dispatcher;
|
||||
}
|
||||
|
||||
public IDispatcher Dispatcher { get; }
|
||||
public bool Called { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Called = true;
|
||||
Assert.Same(Dispatcher, SynchronizationContext.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -13,6 +19,10 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
|||
{
|
||||
public class CircuitPrerendererTest
|
||||
{
|
||||
private static readonly Regex ContentWrapperRegex = new Regex(
|
||||
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"0\",\"componentId\":\"0\"}} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
|
||||
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
|
||||
|
||||
// Because CircuitPrerenderer is a point of integration with HttpContext,
|
||||
// it's not a good candidate for unit testing. The majority of prerendering
|
||||
// unit tests should be elsewhere in HtmlRendererTests inside the
|
||||
|
|
@ -26,32 +36,40 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
|||
{
|
||||
// Arrange
|
||||
var circuitFactory = new TestCircuitFactory();
|
||||
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory);
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var httpRequest = new Mock<HttpRequest>().SetupAllProperties();
|
||||
httpContext.Setup(h => h.Request).Returns(httpRequest.Object);
|
||||
httpRequest.Object.Scheme = "https";
|
||||
httpRequest.Object.Host = new HostString("example.com", 1234);
|
||||
httpRequest.Object.Path = "/some/path";
|
||||
var circuitRegistry = new CircuitRegistry(Options.Create(new CircuitOptions()), Mock.Of<ILogger<CircuitRegistry>>());
|
||||
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var httpRequest = httpContext.Request;
|
||||
httpRequest.Scheme = "https";
|
||||
httpRequest.Host = new HostString("example.com", 1234);
|
||||
httpRequest.Path = "/some/path";
|
||||
|
||||
var prerenderingContext = new ComponentPrerenderingContext
|
||||
{
|
||||
ComponentType = typeof(UriDisplayComponent),
|
||||
Parameters = ParameterCollection.Empty,
|
||||
Context = httpContext.Object
|
||||
Context = httpContext
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[]
|
||||
Assert.Equal(string.Join("", new[]
|
||||
{
|
||||
"The current URI is ",
|
||||
"https://example.com:1234/some/path",
|
||||
" within base URI ",
|
||||
"https://example.com:1234/"
|
||||
}, result);
|
||||
}), GetUnwrappedContent(result));
|
||||
}
|
||||
|
||||
private string GetUnwrappedContent(ComponentPrerenderResult rawResult)
|
||||
{
|
||||
var writer = new StringWriter();
|
||||
rawResult.WriteTo(writer);
|
||||
return ContentWrapperRegex.Match(writer.ToString())
|
||||
.Groups["content"].Value
|
||||
.Replace("\r\n","\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -59,33 +77,33 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
|||
{
|
||||
// Arrange
|
||||
var circuitFactory = new TestCircuitFactory();
|
||||
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory);
|
||||
var httpContext = new Mock<HttpContext>();
|
||||
var httpRequest = new Mock<HttpRequest>().SetupAllProperties();
|
||||
httpContext.Setup(h => h.Request).Returns(httpRequest.Object);
|
||||
httpRequest.Object.Scheme = "https";
|
||||
httpRequest.Object.Host = new HostString("example.com", 1234);
|
||||
httpRequest.Object.PathBase = "/my/dir";
|
||||
httpRequest.Object.Path = "/some/path";
|
||||
var circuitRegistry = new CircuitRegistry(Options.Create(new CircuitOptions()), Mock.Of<ILogger<CircuitRegistry>>());
|
||||
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var httpRequest = httpContext.Request;
|
||||
httpRequest.Scheme = "https";
|
||||
httpRequest.Host = new HostString("example.com", 1234);
|
||||
httpRequest.PathBase = "/my/dir";
|
||||
httpRequest.Path = "/some/path";
|
||||
|
||||
var prerenderingContext = new ComponentPrerenderingContext
|
||||
{
|
||||
ComponentType = typeof(UriDisplayComponent),
|
||||
Parameters = ParameterCollection.Empty,
|
||||
Context = httpContext.Object
|
||||
Context = httpContext
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(new[]
|
||||
Assert.Equal(string.Join("", new[]
|
||||
{
|
||||
"The current URI is ",
|
||||
"https://example.com:1234/my/dir/some/path",
|
||||
" within base URI ",
|
||||
"https://example.com:1234/my/dir/"
|
||||
}, result);
|
||||
}), GetUnwrappedContent(result));
|
||||
}
|
||||
|
||||
class TestCircuitFactory : CircuitFactory
|
||||
|
|
@ -95,8 +113,8 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
|||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddScoped<IUriHelper>(_ =>
|
||||
{
|
||||
var uriHelper = new RemoteUriHelper();
|
||||
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
|
||||
var uriHelper = new RemoteUriHelper(NullLogger<RemoteUriHelper>.Instance);
|
||||
uriHelper.InitializeState(uriAbsolute, baseUriAbsolute);
|
||||
return uriHelper;
|
||||
});
|
||||
var serviceScope = serviceCollection.BuildServiceProvider().CreateScope();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
{
|
||||
protected override HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider)
|
||||
{
|
||||
return GetRemoteRenderer(serviceProvider, CircuitClientProxy.OfflineClient);
|
||||
return GetRemoteRenderer(serviceProvider, new CircuitClientProxy());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
component.TriggerRender();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, renderer.OfflineRenderBatches.Count);
|
||||
Assert.Equal(2, renderer.PendingRenderBatches.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -51,15 +51,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
{
|
||||
// Arrange
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var renderIds = new List<int>();
|
||||
var renderIds = new List<long>();
|
||||
|
||||
var firstBatchTCS = new TaskCompletionSource<object>();
|
||||
var secondBatchTCS = new TaskCompletionSource<object>();
|
||||
var thirdBatchTCS = new TaskCompletionSource<object>();
|
||||
|
||||
var initialClient = new Mock<IClientProxy>();
|
||||
initialClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback((string name, object[] value, CancellationToken token) =>
|
||||
{
|
||||
renderIds.Add((int)value[1]);
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
||||
.Returns(firstBatchTCS.Task);
|
||||
var circuitClient = new CircuitClientProxy(initialClient.Object, "connection0");
|
||||
var renderer = GetRemoteRenderer(serviceProvider, circuitClient);
|
||||
var component = new TestComponent(builder =>
|
||||
|
|
@ -68,18 +69,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
});
|
||||
|
||||
|
||||
var client = new Mock<IClientProxy>();
|
||||
client.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback((string name, object[] value, CancellationToken token) =>
|
||||
{
|
||||
renderIds.Add((int)value[1]);
|
||||
})
|
||||
.Returns(Task.CompletedTask);
|
||||
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
||||
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 3 ? secondBatchTCS.Task : thirdBatchTCS.Task);
|
||||
|
||||
var componentId = renderer.AssignRootComponentId(component);
|
||||
component.TriggerRender();
|
||||
renderer.OnRenderCompleted(1, null);
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
firstBatchTCS.SetResult(null);
|
||||
|
||||
circuitClient.SetDisconnected();
|
||||
component.TriggerRender();
|
||||
|
|
@ -88,22 +87,174 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
// Act
|
||||
circuitClient.Transfer(client.Object, "new-connection");
|
||||
var task = renderer.ProcessBufferedRenderBatches();
|
||||
foreach (var id in renderIds)
|
||||
|
||||
foreach (var id in renderIds.ToArray())
|
||||
{
|
||||
renderer.OnRenderCompleted(id, null);
|
||||
}
|
||||
await task;
|
||||
|
||||
secondBatchTCS.SetResult(null);
|
||||
thirdBatchTCS.SetResult(null);
|
||||
|
||||
// Assert
|
||||
client.Verify(c => c.SendCoreAsync("JS.RenderBatch", It.IsAny<object[]>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||
Assert.Equal(new long[] { 2, 3, 4 }, renderIds);
|
||||
Assert.True(task.Wait(3000), "One or more render batches werent acknowledged");
|
||||
|
||||
await task;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnRenderCompletedAsync_ThrowsWhenNoBatchesAreQueued()
|
||||
{
|
||||
// Arrange
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var firstBatchTCS = new TaskCompletionSource<object>();
|
||||
var secondBatchTCS = new TaskCompletionSource<object>();
|
||||
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
|
||||
offlineClient.SetDisconnected();
|
||||
var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
|
||||
RenderFragment initialContent = (builder) =>
|
||||
{
|
||||
builder.OpenElement(0, "my element");
|
||||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
};
|
||||
var trigger = new Trigger();
|
||||
var renderIds = new List<long>();
|
||||
var onlineClient = new Mock<IClientProxy>();
|
||||
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
||||
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
|
||||
|
||||
// This produces the initial batch (id = 2)
|
||||
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
|
||||
ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(AutoParameterTestComponent.Content)] = initialContent,
|
||||
[nameof(AutoParameterTestComponent.Trigger)] = trigger
|
||||
}));
|
||||
trigger.Component.Content = (builder) =>
|
||||
{
|
||||
builder.OpenElement(0, "offline element");
|
||||
builder.AddContent(1, "offline text");
|
||||
builder.CloseElement();
|
||||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.PendingRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||
var task = renderer.ProcessBufferedRenderBatches();
|
||||
var exceptions = new List<Exception>();
|
||||
renderer.UnhandledException += (sender, e) =>
|
||||
{
|
||||
exceptions.Add(e);
|
||||
};
|
||||
|
||||
// Pretend that we missed the ack for the initial batch
|
||||
renderer.OnRenderCompleted(2, null);
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
|
||||
// Assert
|
||||
var exception = Assert.Single(exceptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThrowsIfWeReceiveAnOutOfSequenceClientAcknowledge()
|
||||
{
|
||||
// Arrange
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
var firstBatchTCS = new TaskCompletionSource<object>();
|
||||
var secondBatchTCS = new TaskCompletionSource<object>();
|
||||
var offlineClient = new CircuitClientProxy(new Mock<IClientProxy>(MockBehavior.Strict).Object, "offline-client");
|
||||
offlineClient.SetDisconnected();
|
||||
var renderer = GetRemoteRenderer(serviceProvider, offlineClient);
|
||||
RenderFragment initialContent = (builder) =>
|
||||
{
|
||||
builder.OpenElement(0, "my element");
|
||||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
};
|
||||
var trigger = new Trigger();
|
||||
var renderIds = new List<long>();
|
||||
var onlineClient = new Mock<IClientProxy>();
|
||||
onlineClient.Setup(c => c.SendCoreAsync(It.IsAny<string>(), It.IsAny<object[]>(), It.IsAny<CancellationToken>()))
|
||||
.Callback((string name, object[] value, CancellationToken token) => renderIds.Add((long)value[1]))
|
||||
.Returns<string, object[], CancellationToken>((n, v, t) => (long)v[1] == 2 ? firstBatchTCS.Task : secondBatchTCS.Task);
|
||||
|
||||
// This produces the initial batch (id = 2)
|
||||
var result = await renderer.RenderComponentAsync<AutoParameterTestComponent>(
|
||||
ParameterCollection.FromDictionary(new Dictionary<string, object>
|
||||
{
|
||||
[nameof(AutoParameterTestComponent.Content)] = initialContent,
|
||||
[nameof(AutoParameterTestComponent.Trigger)] = trigger
|
||||
}));
|
||||
trigger.Component.Content = (builder) =>
|
||||
{
|
||||
builder.OpenElement(0, "offline element");
|
||||
builder.AddContent(1, "offline text");
|
||||
builder.CloseElement();
|
||||
};
|
||||
// This produces an additional batch (id = 3)
|
||||
trigger.TriggerRender();
|
||||
var originallyQueuedBatches = renderer.PendingRenderBatches.Count;
|
||||
|
||||
// Act
|
||||
offlineClient.Transfer(onlineClient.Object, "new-connection");
|
||||
var task = renderer.ProcessBufferedRenderBatches();
|
||||
var exceptions = new List<Exception>();
|
||||
renderer.UnhandledException += (sender, e) =>
|
||||
{
|
||||
exceptions.Add(e);
|
||||
};
|
||||
|
||||
// Pretend that we missed the ack for the initial batch
|
||||
renderer.OnRenderCompleted(3, null);
|
||||
firstBatchTCS.SetResult(null);
|
||||
secondBatchTCS.SetResult(null);
|
||||
|
||||
// Assert
|
||||
var exception = Assert.Single(exceptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrerendersMultipleComponentsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var serviceProvider = new ServiceCollection().BuildServiceProvider();
|
||||
|
||||
var renderer = GetRemoteRenderer(
|
||||
serviceProvider,
|
||||
new CircuitClientProxy());
|
||||
|
||||
// Act
|
||||
var first = await renderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty);
|
||||
var second = await renderer.RenderComponentAsync<TestComponent>(ParameterCollection.Empty);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(0, first.ComponentId);
|
||||
Assert.Equal(1, second.ComponentId);
|
||||
Assert.Equal(2, renderer.PendingRenderBatches.Count);
|
||||
}
|
||||
|
||||
private RemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClientProxy)
|
||||
{
|
||||
var jsRuntime = new Mock<IJSRuntime>();
|
||||
jsRuntime.Setup(r => r.InvokeAsync<object>(
|
||||
"Blazor._internal.attachRootComponentToElement",
|
||||
It.IsAny<int>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<int>()))
|
||||
.ReturnsAsync(Task.FromResult<object>(null));
|
||||
|
||||
return new RemoteRenderer(
|
||||
serviceProvider,
|
||||
new RendererRegistry(),
|
||||
Mock.Of<IJSRuntime>(),
|
||||
jsRuntime.Object,
|
||||
circuitClientProxy,
|
||||
Dispatcher,
|
||||
HtmlEncoder.Default,
|
||||
|
|
@ -113,7 +264,16 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
private class TestComponent : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
private RenderFragment _renderFragment;
|
||||
private RenderFragment _renderFragment = (builder) =>
|
||||
{
|
||||
builder.OpenElement(0, "my element");
|
||||
builder.AddContent(1, "some text");
|
||||
builder.CloseElement();
|
||||
};
|
||||
|
||||
public TestComponent()
|
||||
{
|
||||
}
|
||||
|
||||
public TestComponent(RenderFragment renderFragment)
|
||||
{
|
||||
|
|
@ -137,5 +297,43 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
|
|||
Assert.True(task.IsCompletedSuccessfully);
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoParameterTestComponent : IComponent
|
||||
{
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
[Parameter] public RenderFragment Content { get; set; }
|
||||
|
||||
[Parameter] public Trigger Trigger { get; set; }
|
||||
|
||||
public void Configure(RenderHandle renderHandle)
|
||||
{
|
||||
_renderHandle = renderHandle;
|
||||
}
|
||||
|
||||
public Task SetParametersAsync(ParameterCollection parameters)
|
||||
{
|
||||
Content = parameters.GetValueOrDefault<RenderFragment>(nameof(Content));
|
||||
Trigger ??= parameters.GetValueOrDefault<Trigger>(nameof(Trigger));
|
||||
Trigger.Component = this;
|
||||
TriggerRender();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void TriggerRender()
|
||||
{
|
||||
var task = _renderHandle.Invoke(() => _renderHandle.Render(Content));
|
||||
Assert.True(task.IsCompletedSuccessfully);
|
||||
}
|
||||
}
|
||||
|
||||
private class Trigger
|
||||
{
|
||||
public AutoParameterTestComponent Component { get; set; }
|
||||
public void TriggerRender()
|
||||
{
|
||||
Component.TriggerRender();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
|
|
@ -25,10 +26,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
_serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost;
|
||||
}
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
Navigate("/", noReload: false);
|
||||
WaitUntilLoaded();
|
||||
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
|
||||
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("blazorpack")));
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -161,10 +166,23 @@ window.Blazor._internal.forceCloseConnection();");
|
|||
_ => element.Text != currentValue);
|
||||
}
|
||||
|
||||
private void WaitUntilLoaded()
|
||||
[Fact]
|
||||
public void RendersContinueAfterPrerendering()
|
||||
{
|
||||
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
|
||||
driver => driver.FindElement(By.TagName("app")).Text != "Loading...");
|
||||
Browser.FindElement(By.LinkText("Greeter")).Click();
|
||||
Browser.Equal("Hello Guest", () => Browser.FindElement(By.ClassName("greeting")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorsStopTheRenderingProcess()
|
||||
{
|
||||
Browser.FindElement(By.LinkText("Error")).Click();
|
||||
Browser.Equal("Error", () => Browser.FindElement(By.CssSelector("h1")).Text);
|
||||
|
||||
Browser.FindElement(By.Id("cause-error")).Click();
|
||||
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
|
||||
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("Connection disconnected.")));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,8 +459,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
|||
public void CanUseJsInteropForRefElementsDuringOnAfterRender()
|
||||
{
|
||||
var appElement = MountTestComponent<AfterRenderInteropComponent>();
|
||||
var inputElement = appElement.FindElement(By.TagName("input"));
|
||||
Assert.Equal("Value set after render", inputElement.GetAttribute("value"));
|
||||
Browser.Equal("Value set after render", () => Browser.FindElement(By.TagName("input")).GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
@using Microsoft.AspNetCore.Components;
|
||||
<!--
|
||||
Configuring this stuff here is temporary. Later we'll move the app config
|
||||
into Startup.cs, and it won't be necessary to specify AppAssembly.
|
||||
-->
|
||||
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
|
||||
<CascadingValue Value="Name" Name="Name" IsFixed=true>
|
||||
<Router AppAssembly=typeof(ComponentsApp.App.App).Assembly />
|
||||
</CascadingValue>
|
||||
|
||||
@functions{
|
||||
[Parameter] public string Name { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -27,6 +27,16 @@
|
|||
<span class="oi oi-list-rich" aria-hidden="true"></span> Ticker
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="greeter">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Greeter
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="error">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Error
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Server" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<ProjectReference Include="..\ComponentsApp.App\ComponentsApp.App.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@page "{*clientroutes}"
|
||||
@page
|
||||
@using ComponentsApp.App
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -7,13 +7,15 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Razor Components</title>
|
||||
<base href="/" />
|
||||
<base href="~/" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/site.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<app>@(await Html.RenderComponentAsync<App>())</app>
|
||||
|
||||
<app>@(await Html.RenderComponentAsync<App>(new { Name="Guest" }))</app>
|
||||
|
||||
<script src="_framework/components.server.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Server.Circuits;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
|
@ -35,7 +36,9 @@ namespace ComponentsApp.Server
|
|||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapComponentHub<App.App>("app");
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapComponentHub();
|
||||
endpoints.MapFallbackToPage("/Index");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -1,5 +1,6 @@
|
|||
@page
|
||||
@using BasicTestApp.RouterTest
|
||||
@using Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Server" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"System": "Debug",
|
||||
"Microsoft": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"devDependencies": {
|
||||
"jest": "^23.6.0",
|
||||
"merge": "^1.2.1",
|
||||
"puppeteer": "^1.13.0"
|
||||
"puppeteer": "^1.14.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -2826,10 +2826,10 @@ punycode@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
puppeteer@^1.13.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.13.0.tgz#187ccf5ed5caf08ed1291b262d033cc364bf88ab"
|
||||
integrity sha512-LUXgvhjfB/P6IOUDAKxOcbCz9ISwBLL9UpKghYrcBDwrOGx1m60y0iN2M64mdAUbT4+7oZM5DTxOW7equa2fxQ==
|
||||
puppeteer@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339"
|
||||
integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw==
|
||||
dependencies:
|
||||
debug "^4.1.0"
|
||||
extract-zip "^1.6.6"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,20 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Server
|
||||
{
|
||||
public partial class ComponentPrerenderingContext
|
||||
{
|
||||
public ComponentPrerenderingContext() { }
|
||||
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Http.HttpContext Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public Microsoft.AspNetCore.Components.ParameterCollection Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
}
|
||||
public partial interface IComponentPrerenderer
|
||||
{
|
||||
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<string>> PrerenderComponentAsync(Microsoft.AspNetCore.Components.Server.ComponentPrerenderingContext context);
|
||||
}
|
||||
}
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
[System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
|
||||
|
|
@ -993,9 +979,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
}
|
||||
public static partial class HtmlHelperRazorComponentExtensions
|
||||
{
|
||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
|
||||
}
|
||||
public partial class HtmlHelper<TModel> : Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper, Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
|
@ -206,7 +205,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
//
|
||||
// Component prerendering
|
||||
//
|
||||
services.TryAddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
|
||||
services.TryAddScoped<StaticComponentRenderer>();
|
||||
services.TryAddScoped<IUriHelper, HttpUriHelper>();
|
||||
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
|
||||
|
|
@ -22,14 +21,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
/// </summary>
|
||||
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
|
||||
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
|
||||
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
|
||||
public static Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
|
||||
{
|
||||
if (htmlHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(htmlHelper));
|
||||
}
|
||||
|
||||
return htmlHelper.RenderComponentAsync<TComponent>(null);
|
||||
return htmlHelper.RenderStaticComponentAsync<TComponent>(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
|
||||
/// to the component.</param>
|
||||
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
|
||||
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
|
||||
public static async Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(
|
||||
this IHtmlHelper htmlHelper,
|
||||
object parameters) where TComponent : IComponent
|
||||
{
|
||||
|
|
@ -50,14 +49,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
|||
|
||||
var httpContext = htmlHelper.ViewContext.HttpContext;
|
||||
var serviceProvider = httpContext.RequestServices;
|
||||
var prerenderer = serviceProvider.GetRequiredService<IComponentPrerenderer>();
|
||||
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
||||
|
||||
var result = await prerenderer.PrerenderComponentAsync(new ComponentPrerenderingContext
|
||||
{
|
||||
Context = httpContext,
|
||||
ComponentType = typeof(TComponent),
|
||||
Parameters = parameters == null ? ParameterCollection.Empty : ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))
|
||||
});
|
||||
var parametersCollection = parameters == null ?
|
||||
ParameterCollection.Empty :
|
||||
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
|
||||
|
||||
var result = await prerenderer.PrerenderComponentAsync(
|
||||
parametersCollection,
|
||||
httpContext,
|
||||
typeof(TComponent));
|
||||
|
||||
return new ComponentHtmlContent(result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,57 +3,17 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||
{
|
||||
internal class HttpUriHelper : UriHelperBase
|
||||
{
|
||||
private HttpContext _context;
|
||||
|
||||
public HttpUriHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public void InitializeState(HttpContext context)
|
||||
{
|
||||
_context = context;
|
||||
InitializeState();
|
||||
}
|
||||
|
||||
protected override void InitializeState()
|
||||
{
|
||||
if (_context == null)
|
||||
{
|
||||
throw new InvalidOperationException($"'{typeof(HttpUriHelper)}' not initialized.");
|
||||
}
|
||||
SetAbsoluteBaseUri(GetContextBaseUri());
|
||||
SetAbsoluteUri(GetFullUri());
|
||||
}
|
||||
|
||||
private string GetFullUri()
|
||||
{
|
||||
var request = _context.Request;
|
||||
return UriHelper.BuildAbsolute(
|
||||
request.Scheme,
|
||||
request.Host,
|
||||
request.PathBase,
|
||||
request.Path,
|
||||
request.QueryString);
|
||||
}
|
||||
|
||||
private string GetContextBaseUri()
|
||||
{
|
||||
var request = _context.Request;
|
||||
return UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
|
||||
}
|
||||
|
||||
protected override void NavigateToCore(string uri, bool forceLoad)
|
||||
{
|
||||
// For now throw as we don't have a good way of aborting the request from here.
|
||||
throw new InvalidOperationException(
|
||||
"Redirects are not supported on a prerendering environment.");
|
||||
throw new InvalidOperationException("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
|
||||
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
|
||||
"attempted during prerendering.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 += "/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ using System.Text.Encodings.Web;
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
|
@ -29,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var writer = new StringWriter();
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<TestComponent>();
|
||||
var result = await helper.RenderStaticComponentAsync<TestComponent>();
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var writer = new StringWriter();
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<GreetingComponent>(new
|
||||
var result = await helper.RenderStaticComponentAsync<GreetingComponent>(new
|
||||
{
|
||||
Name = "Steve"
|
||||
});
|
||||
|
|
@ -63,7 +62,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var helper = CreateHelper();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
|
||||
{
|
||||
IsAsync = false
|
||||
}));
|
||||
|
|
@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var helper = CreateHelper();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
|
||||
{
|
||||
IsAsync = true
|
||||
}));
|
||||
|
|
@ -95,7 +94,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var helper = CreateHelper();
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
|
||||
{
|
||||
JsInterop = true
|
||||
}));
|
||||
|
|
@ -122,16 +121,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
var writer = new StringWriter();
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<RedirectComponent>(new
|
||||
{
|
||||
RedirectUri = "http://localhost/redirect"
|
||||
}));
|
||||
|
||||
Assert.Equal("Redirects are not supported on a prerendering environment.", exception.Message);
|
||||
Assert.Equal("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
|
||||
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
|
||||
"attempted during prerendering.",
|
||||
exception.Message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CanRender_AsyncComponent()
|
||||
{
|
||||
|
|
@ -182,7 +182,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
</table>";
|
||||
|
||||
// Act
|
||||
var result = await helper.RenderComponentAsync<AsyncComponent>();
|
||||
var result = await helper.RenderStaticComponentAsync<AsyncComponent>();
|
||||
result.WriteTo(writer, HtmlEncoder.Default);
|
||||
var content = writer.ToString();
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
|||
services.AddSingleton(HtmlEncoder.Default);
|
||||
services.AddSingleton<IJSRuntime,UnsupportedJavaScriptRuntime>();
|
||||
services.AddSingleton<IUriHelper,HttpUriHelper>();
|
||||
services.AddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
|
||||
services.AddSingleton<StaticComponentRenderer>();
|
||||
|
||||
configureServices?.Invoke(services);
|
||||
|
||||
|
|
|
|||
|
|
@ -317,6 +317,12 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FSharpWebSite", "test\WebSi
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{A8C3066F-E80D-4E03-9962-741B551B8FBC}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mvc.Components.Prerendering", "Mvc.Components.Prerendering", "{45CE788D-4B69-4F83-981C-F43D8F15B0F1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering", "Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", "{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering.Test", "Mvc.Components.Prerendering\test\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj", "{C6F3BCE6-1EFD-4360-932B-B98573E78926}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -1809,6 +1815,30 @@ Global
|
|||
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A8C3066F-E80D-4E03-9962-741B551B8FBC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|Mixed Platforms.Build.0 = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -1938,6 +1968,8 @@ Global
|
|||
{F99CAC82-C96E-41F4-AA28-1BBBD09C447A} = {5FE3048A-E96B-44F8-A7C4-FC590D7E04B4}
|
||||
{65E98187-96FB-4FCD-94A3-F8048C2F13F1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{A8C3066F-E80D-4E03-9962-741B551B8FBC} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
|
||||
{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
|
||||
{C6F3BCE6-1EFD-4360-932B-B98573E78926} = {45CE788D-4B69-4F83-981C-F43D8F15B0F1}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {63D344F6-F86D-40E6-85B9-0AABBE338C4A}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Server" />
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
|
||||
|
||||
<Reference Include="Microsoft.AspNetCore.Razor.Runtime" />
|
||||
<Reference Include="Microsoft.Extensions.WebEncoders" />
|
||||
|
|
|
|||
|
|
@ -478,6 +478,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Act
|
||||
var response = await Client.GetStringAsync("Home/GetAssemblyPartData");
|
||||
var assemblyParts = JsonConvert.DeserializeObject<IList<string>>(response);
|
||||
var expected = new[]
|
||||
{
|
||||
"BasicWebSite",
|
||||
"Microsoft.AspNetCore.Components.Server",
|
||||
"Microsoft.AspNetCore.Mvc.Components.Prerendering",
|
||||
"Microsoft.AspNetCore.SpaServices",
|
||||
"Microsoft.AspNetCore.SpaServices.Extensions",
|
||||
"Microsoft.AspNetCore.Mvc.TagHelpers",
|
||||
"Microsoft.AspNetCore.Mvc.Razor",
|
||||
};
|
||||
|
||||
// Assert
|
||||
//
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp.Parser.Html;
|
||||
using BasicWebSite;
|
||||
using BasicWebSite.Services;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -16,6 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
public class ComponentRenderingFunctionalTests : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
|
||||
{
|
||||
private static readonly Regex ContentWrapperRegex = new Regex(
|
||||
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"\\d+\",\"componentId\":\"\\d+\"}} -->(?<content>.*)<!-- M.A.C.Component: \\d+ -->",
|
||||
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
|
||||
|
||||
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||
{
|
||||
Factory = fixture;
|
||||
|
|
@ -35,14 +40,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
|
||||
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
|
||||
public async Task Renders_BasicComponent_UsingRazorComponents_Prerenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
var client = CreateClient(Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components");
|
||||
|
||||
|
|
@ -50,14 +56,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
|
||||
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_RoutingComponent()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/routable");
|
||||
|
||||
|
|
@ -65,14 +71,15 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
|
||||
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
|
||||
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
var client = CreateClient(Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/routable");
|
||||
|
||||
|
|
@ -80,14 +87,46 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
|
||||
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerrenderer()
|
||||
public async Task Renders_BasicComponentInteractive_UsingRazorComponents_Prerenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory, builder => builder.ConfigureServices(services => services.AddRazorComponents()));
|
||||
var client = CreateClient(Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/false");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("<p>Hello world!</p>", "Greetings", content, unwrap: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_RoutingComponentInteractive_UsingRazorComponents_Prerenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/routable/false");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content, unwrap: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Renders_ThrowingComponent_UsingRazorComponents_Prerenderer()
|
||||
{
|
||||
// Arrange & Act
|
||||
var client = CreateClient(Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents())));
|
||||
|
||||
var response = await client.GetAsync("http://localhost/components/throws");
|
||||
|
||||
|
|
@ -103,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
// Arrange & Act
|
||||
var expectedHtml = @"
|
||||
<h1>Weather forecast</h1>
|
||||
<h1>Weather forecast</h1>
|
||||
|
||||
<p>This component demonstrates fetching data from the server.</p>
|
||||
|
||||
|
|
@ -150,7 +189,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
";
|
||||
var client = CreateClient(Factory);
|
||||
var response = await client.GetAsync("http://localhost/components");
|
||||
|
|
@ -162,14 +200,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
AssertComponent(expectedHtml, "FetchData", content);
|
||||
}
|
||||
|
||||
private void AssertComponent(string expectedConent, string divId, string responseContent)
|
||||
private void AssertComponent(string expectedContent, string divId, string responseContent, bool unwrap = false)
|
||||
{
|
||||
var parser = new HtmlParser();
|
||||
var htmlDocument = parser.Parse(responseContent);
|
||||
var div = htmlDocument.Body.QuerySelector($"#{divId}");
|
||||
var content = unwrap ? GetUnwrappedContent(div.InnerHtml) : div.InnerHtml;
|
||||
Assert.Equal(
|
||||
expectedConent.Replace("\r\n","\n"),
|
||||
div.InnerHtml.Replace("\r\n","\n"));
|
||||
expectedContent.Replace("\r\n","\n"),
|
||||
content.Replace("\r\n","\n"));
|
||||
}
|
||||
|
||||
private string GetUnwrappedContent(string rawResult)
|
||||
{
|
||||
return ContentWrapperRegex.Match(rawResult)
|
||||
.Groups["content"].Value
|
||||
.Replace("\r\n", "\n");
|
||||
}
|
||||
|
||||
// A simple delegating handler used in setting up test services so that we can configure
|
||||
|
|
@ -178,16 +224,12 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
{
|
||||
}
|
||||
|
||||
private HttpClient CreateClient(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture, Action<IWebHostBuilder> configure = null)
|
||||
private HttpClient CreateClient(WebApplicationFactory<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||
{
|
||||
var loopHandler = new LoopHttpHandler();
|
||||
|
||||
var client = fixture
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
configure?.Invoke(builder);
|
||||
builder.ConfigureServices(ConfigureTestWeatherForecastService);
|
||||
})
|
||||
.WithWebHostBuilder(builder => builder.ConfigureServices(ConfigureTestWeatherForecastService))
|
||||
.CreateClient();
|
||||
|
||||
// We configure the inner handler with a handler to this TestServer instance so that calls to the
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
|
||||
|
||||
<Reference Include="Microsoft.AspNetCore.Authentication" />
|
||||
<Reference Include="Microsoft.AspNetCore.Components.Server" />
|
||||
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
|
||||
<Reference Include="Microsoft.AspNetCore.Localization.Routing" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BasicWebSite.Controllers
|
||||
|
|
@ -50,11 +51,16 @@ namespace BasicWebSite.Controllers
|
|||
}
|
||||
};
|
||||
|
||||
[HttpGet("/components")]
|
||||
[HttpGet("/components/{component}")]
|
||||
public IActionResult Index()
|
||||
[HttpGet("/components/{staticPrerender=true}")]
|
||||
[HttpGet("/components/routable/{staticPrerender=true}")]
|
||||
public IActionResult Index(bool staticPrerender)
|
||||
{
|
||||
return View();
|
||||
// Override the path so that the router finds the RoutedPage component
|
||||
// as the client router doesn't support optional parameters.
|
||||
Request.Path = Request.Path.StartsWithSegments("/components/routable") ?
|
||||
PathString.FromUriComponent("/components/routable") : Request.Path;
|
||||
|
||||
return View(staticPrerender);
|
||||
}
|
||||
|
||||
[HttpGet("/WeatherData")]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,35 @@
|
|||
@using BasicWebSite.RazorComponents;
|
||||
@model bool;
|
||||
<h1>Razor components</h1>
|
||||
<div id="Greetings">
|
||||
@(await Html.RenderComponentAsync<Greetings>())
|
||||
@if (Model)
|
||||
{
|
||||
@(await Html.RenderStaticComponentAsync<Greetings>())
|
||||
}
|
||||
else
|
||||
{
|
||||
@(await Html.RenderComponentAsync<Greetings>())
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="FetchData">
|
||||
@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
|
||||
@if (Model)
|
||||
{
|
||||
@(await Html.RenderStaticComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
|
||||
}
|
||||
else
|
||||
{
|
||||
@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="Routing">
|
||||
@(await Html.RenderComponentAsync<RouterContainer>())
|
||||
@if (Model)
|
||||
{
|
||||
@(await Html.RenderStaticComponentAsync<RouterContainer>());
|
||||
}
|
||||
else
|
||||
{
|
||||
@(await Html.RenderComponentAsync<RouterContainer>());
|
||||
}
|
||||
</div>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Azure\AzureAD\Authentication.AzureADB2C.UI\src\Microsoft.AspNetCore.Authentication.AzureADB2C.UI.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Components\src\Microsoft.AspNetCore.Components.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Browser\src\Microsoft.AspNetCore.Components.Browser.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Identity\EntityFrameworkCore\src\Microsoft.AspNetCore.Identity.EntityFrameworkCore.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Identity\UI\src\Microsoft.AspNetCore.Identity.UI.csproj" />
|
||||
<PackageVersionVariableReference Include="$(RepositoryRoot)src\Middleware\Diagnostics.EntityFrameworkCore\src\Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.csproj" />
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Server" Version="${MicrosoftAspNetCoreComponentsServerPackageVersion}" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" Version="${MicrosoftAspNetCoreMvcComponentsPrerenderingPackageVersion}" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="${MicrosoftAspNetCoreMvcNewtonsoftJsonPackageVersion}" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@page "/{*clientPath}"
|
||||
@page "/"
|
||||
@namespace RazorComponentsWeb_CSharp.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ namespace RazorComponentsWeb_CSharp
|
|||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapComponentHub<App>("app");
|
||||
endpoints.MapComponentHub();
|
||||
endpoints.MapFallbackToPage("/Host");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
|
@ -16,6 +17,7 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Templates.Test.Helpers
|
||||
{
|
||||
[DebuggerDisplay("{ToString(),nq}")]
|
||||
public class AspNetProcess : IDisposable
|
||||
{
|
||||
private const string ListeningMessagePrefix = "Now listening on: ";
|
||||
|
|
@ -142,5 +144,24 @@ namespace Templates.Test.Helpers
|
|||
_httpClient.Dispose();
|
||||
Process.Dispose();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var result = "";
|
||||
result += Process != null ? "Active: " : "Inactive";
|
||||
if (Process != null)
|
||||
{
|
||||
if (!Process.HasExited)
|
||||
{
|
||||
result += $"(Listening on {_listeningUri.OriginalString}) PID: {Process.Id}";
|
||||
}
|
||||
else
|
||||
{
|
||||
result += "(Already finished)";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ namespace Templates.Test.Helpers
|
|||
|
||||
public int ExitCode => _process.ExitCode;
|
||||
|
||||
public object Id => _process.Id;
|
||||
|
||||
public static ProcessEx Run(ITestOutputHelper output, string workingDirectory, string command, string args = null, IDictionary<string, string> envVars = null)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo(command, args)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
|
@ -15,6 +16,7 @@ using Xunit.Sdk;
|
|||
|
||||
namespace Templates.Test.Helpers
|
||||
{
|
||||
[DebuggerDisplay("{ToString(),nq}")]
|
||||
public class Project
|
||||
{
|
||||
public const string DefaultFramework = "netcoreapp3.0";
|
||||
|
|
@ -384,5 +386,7 @@ namespace Templates.Test.Helpers
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{ProjectName}: {TemplateOutputDir}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue