Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect Blazor WASM projects and add/update appropriate config to allow launching and debugging #3593

Merged
merged 8 commits into from
Feb 26, 2020
142 changes: 116 additions & 26 deletions src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,27 @@ export class AssetGenerator {
throw new Error("Startup project not set");
}

let projectFileText = fs.readFileSync(this.startupProject.Path, 'utf8');
return this.startupProject.IsWebProject;
}

public computeProgramLaunchType(): ProgramLaunchType {
if (!this.startupProject) {
throw new Error("Startup project not set");
}

if (this.startupProject.IsBlazorWebAssemblyStandalone) {
return ProgramLaunchType.BlazorWebAssemblyStandalone;
}

if (this.startupProject.IsBlazorWebAssemblyHosted) {
return ProgramLaunchType.BlazorWebAssemblyHosted;
}

if (this.startupProject.IsWebProject) {
return ProgramLaunchType.Web;
}

// Assume that this is an MSBuild project. In that case, look for the 'Sdk="Microsoft.NET.Sdk.Web"' attribute.
// TODO: Have OmniSharp provide the list of SDKs used by a project and check that list instead.
return projectFileText.toLowerCase().indexOf('sdk="microsoft.net.sdk.web"') >= 0;
return ProgramLaunchType.Console;
}

private computeProgramPath() {
Expand All @@ -160,24 +176,44 @@ export class AssetGenerator {
return path.join('${workspaceFolder}', path.relative(this.workspaceFolder.uri.fsPath, startupProjectDir));
}

public createLaunchJson(isWebProject: boolean): string {
if (!isWebProject) {
const launchConfigurationsMassaged: string = indentJsonString(createLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory()));
const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration());
return `
public createLaunchJsonConfigurations(programLaunchType: ProgramLaunchType): string {
switch (programLaunchType) {
case ProgramLaunchType.Console: {
const launchConfigurationsMassaged: string = indentJsonString(createLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory()));
const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration());
return `
[
${launchConfigurationsMassaged},
${attachConfigurationsMassaged}
]`;
}
else {
const webLaunchConfigurationsMassaged: string = indentJsonString(createWebLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory()));
const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration());
return `
}
case ProgramLaunchType.Web: {
const webLaunchConfigurationsMassaged: string = indentJsonString(createWebLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory()));
const attachConfigurationsMassaged: string = indentJsonString(createAttachConfiguration());
return `
[
${webLaunchConfigurationsMassaged},
${attachConfigurationsMassaged}
]`;
}
case ProgramLaunchType.BlazorWebAssemblyHosted: {
const chromeLaunchConfigurationsMassaged: string = indentJsonString(createBlazorWebAssemblyLaunchConfiguration(this.computeWorkingDirectory()));
const hostedLaunchConfigurationsMassaged: string = indentJsonString(createBlazorWebAssemblyHostedLaunchConfiguration(this.computeProgramPath(), this.computeWorkingDirectory()));
return `
[
${hostedLaunchConfigurationsMassaged},
${chromeLaunchConfigurationsMassaged}
]`;
}
case ProgramLaunchType.BlazorWebAssemblyStandalone: {
const chromeLaunchConfigurationsMassaged: string = indentJsonString(createBlazorWebAssemblyLaunchConfiguration(this.computeWorkingDirectory()));
const devServerLaunchConfigurationMassaged: string = indentJsonString(createBlazorWebAssemblyDevServerLaunchConfiguration(this.computeWorkingDirectory()));
return `
[
${devServerLaunchConfigurationMassaged},
${chromeLaunchConfigurationsMassaged}
]`;
}
}
}

Expand All @@ -195,12 +231,12 @@ export class AssetGenerator {
};
}


private createPublishTaskDescription(): tasks.TaskDescription {
let commandArgs = ['publish'];

this.AddAdditionalCommandArgs(commandArgs);

return {
label: 'publish',
command: 'dotnet',
Expand All @@ -209,12 +245,12 @@ export class AssetGenerator {
problemMatcher: '$msCompile'
};
}

private createWatchTaskDescription(): tasks.TaskDescription {
let commandArgs = ['watch','run'];
let commandArgs = ['watch', 'run'];

this.AddAdditionalCommandArgs(commandArgs);

return {
label: 'watch',
command: 'dotnet',
Expand All @@ -223,7 +259,7 @@ export class AssetGenerator {
problemMatcher: '$msCompile'
};
}

private AddAdditionalCommandArgs(commandArgs: string[]) {
let buildProject = this.startupProject;
if (!buildProject) {
Expand All @@ -237,7 +273,7 @@ export class AssetGenerator {
commandArgs.push("/property:GenerateFullPaths=true");
commandArgs.push("/consoleloggerparameters:NoSummary");
}

public createTasksConfiguration(): tasks.TaskConfiguration {
return {
version: "2.0.0",
Expand All @@ -246,6 +282,13 @@ export class AssetGenerator {
}
}

export enum ProgramLaunchType {
Console,
Web,
BlazorWebAssemblyHosted,
BlazorWebAssemblyStandalone,
}

export function createWebLaunchConfiguration(programPath: string, workingDirectory: string): string {
return `
{
Expand All @@ -272,6 +315,53 @@ export function createWebLaunchConfiguration(programPath: string, workingDirecto
}`;
}

export function createBlazorWebAssemblyHostedLaunchConfiguration(programPath: string, workingDirectory: string): string {
return `
{
"name": ".NET Core Launch (Blazor Hosted)",
"type": "coreclr",
"request": "launch",
// If you have changed target frameworks, make sure to update the program path.
"program": "${util.convertNativePathToPosix(programPath)}",
"args": [],
"cwd": "${util.convertNativePathToPosix(workingDirectory)}",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"preLaunchTask": "build"
}`;
}

export function createBlazorWebAssemblyLaunchConfiguration(workingDirectory: string): string {
return `
{
"name": ".NET Core Debug Blazor Web Assembly in Chrome",
"type": "pwa-chrome",
WardenGnaw marked this conversation as resolved.
Show resolved Hide resolved
"request": "launch",
"timeout": 30000,
// If you have changed the default port / launch URL make sure to update the expectation below
"url": "https://localhost:5001",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as the https variant given I assume we want that as the default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Technically it would be possible to infer the HTTPS port from launchSettings.json but 5001 is indeed the default.

Inferring it doesn't make a huge difference because (1) the default is already correct, and (2) for a developer who's in the habit of changing it, once they've generated launch.json there would be nothing keeping it in sync with subsequent changes in launchSettings.json.

So basically I think this is fine!

"webRoot": "${util.convertNativePathToPosix(workingDirectory)}",
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}"
}`;
}

export function createBlazorWebAssemblyDevServerLaunchConfiguration(workingDirectory: string): string {
return `
{
"name": ".NET Core Launch (Blazor Standalone)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["run"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good change. I was a bit concerned about people getting tripped up by --no-build before.

"cwd": "${util.convertNativePathToPosix(workingDirectory)}",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
NTaylorMullen marked this conversation as resolved.
Show resolved Hide resolved
}
}`;
}

export function createLaunchConfiguration(programPath: string, workingDirectory: string): string {
return `
{
Expand Down Expand Up @@ -510,10 +600,10 @@ async function addLaunchJsonIfNecessary(generator: AssetGenerator, operations: A
// already exists, and in the command case, we delete the launch.json file, but the VS
// Code API will return old configurations anyway, which we do NOT want.

const isWebProject = generator.hasWebServerDependency();
let launchJson: string = generator.createLaunchJson(isWebProject);
const programLaunchType = generator.computeProgramLaunchType();
const launchJsonConfigurations: string = generator.createLaunchJsonConfigurations(programLaunchType);
const configurationsMassaged: string = indentJsonString(launchJsonConfigurations);

const configurationsMassaged: string = indentJsonString(launchJson);
const launchJsonText = `
{
// Use IntelliSense to find out which attributes exist for C# debugging
Expand Down
4 changes: 2 additions & 2 deletions src/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ export class CSharpConfigurationProvider implements vscode.DebugConfigurationPro
const buildOperations : AssetOperations = await getBuildOperations(generator);
await addTasksJsonIfNecessary(generator, buildOperations);

const isWebProject = generator.hasWebServerDependency();
const launchJson: string = generator.createLaunchJson(isWebProject);
const programLaunchType = generator.computeProgramLaunchType();
const launchJson: string = generator.createLaunchJsonConfigurations(programLaunchType);

// jsonc-parser's parse function parses a JSON string with comments into a JSON object. However, this removes the comments.
return parse(launchJson);
Expand Down
7 changes: 5 additions & 2 deletions src/omnisharp/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ export interface MSBuildProject {
OutputPath: string;
IsExe: boolean;
IsUnityProject: boolean;
IsWebProject: boolean;
IsBlazorWebAssemblyStandalone: boolean;
IsBlazorWebAssemblyHosted: boolean;
}

export interface TargetFramework {
Expand Down Expand Up @@ -790,10 +793,10 @@ export function findExecutableMSBuildProjects(projects: MSBuildProject[]) {
let result: MSBuildProject[] = [];

projects.forEach(project => {
if (project.IsExe && findNetCoreAppTargetFramework(project) !== undefined) {
if (project.IsExe && (findNetCoreAppTargetFramework(project) !== undefined || project.IsBlazorWebAssemblyStandalone)) {
result.push(project);
}
});

return result;
}
}
106 changes: 104 additions & 2 deletions src/omnisharp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fs from 'fs-extra';
import * as glob from 'glob';
import { OmniSharpServer } from './server';
import * as path from 'path';
import * as protocol from './protocol';
import * as vscode from 'vscode';
import { MSBuildProject } from './protocol';

export async function autoComplete(server: OmniSharpServer, request: protocol.AutoCompleteRequest, token: vscode.CancellationToken) {
return server.makeRequest<protocol.AutoCompleteResponse[]>(protocol.Requests.AutoComplete, request, token);
Expand Down Expand Up @@ -64,7 +68,30 @@ export async function requestProjectInformation(server: OmniSharpServer, request
}

export async function requestWorkspaceInformation(server: OmniSharpServer) {
return server.makeRequest<protocol.WorkspaceInformationResponse>(protocol.Requests.Projects);
const response = await server.makeRequest<protocol.WorkspaceInformationResponse>(protocol.Requests.Projects);
if (response.MsBuild && response.MsBuild.Projects) {
const blazorDetectionEnabled = hasBlazorWebAssemblyDebugPrerequisites();

for (const project of response.MsBuild.Projects) {
project.IsWebProject = isWebProject(project);
project.IsBlazorWebAssemblyHosted = blazorDetectionEnabled && isBlazorWebAssemblyHosted(project);
project.IsBlazorWebAssemblyStandalone = blazorDetectionEnabled && !project.IsBlazorWebAssemblyHosted && isBlazorWebAssemblyProject(project);
}

if (!blazorDetectionEnabled && response.MsBuild.Projects.some(project => isBlazorWebAssemblyProject(project))) {
// There's a Blazor Web Assembly project but VSCode isn't configured to debug the WASM code, show a notification
// to help the user configure their VSCode appropriately.
vscode.window.showInformationMessage('Additional setup is required to debug Blazor WebAssembly applications.', 'Learn more', 'Close')
.then(async result => {
if (result === 'Learn more') {
const uriToOpen = vscode.Uri.parse('https://aka.ms/blazordebugging#vscode');
await vscode.commands.executeCommand('vscode.open', uriToOpen);
}
});
}
}

return response;
}

export async function runCodeAction(server: OmniSharpServer, request: protocol.V2.RunCodeActionRequest) {
Expand Down Expand Up @@ -121,4 +148,79 @@ export async function debugTestStop(server: OmniSharpServer, request: protocol.V

export async function isNetCoreProject(project: protocol.MSBuildProject) {
return project.TargetFrameworks.find(tf => tf.ShortName.startsWith('netcoreapp') || tf.ShortName.startsWith('netstandard')) !== undefined;
}
}

function isBlazorWebAssemblyHosted(project: protocol.MSBuildProject): boolean {
if (!isBlazorWebAssemblyProject(project)) {
return false;
}

if (!project.IsExe) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, why is this check important? I'd think the latter two checks would suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just following suite with how O# typically checks "is runnable web" project.

return false;
}

if (!project.IsWebProject) {
return false;
}

if (protocol.findNetCoreAppTargetFramework(project) === undefined) {
return false;
}

return true;
}

function isBlazorWebAssemblyProject(project: MSBuildProject): boolean {
const projectDirectory = path.dirname(project.Path);
const launchSettings = glob.sync('**/launchSettings.json', { cwd: projectDirectory });
if (!launchSettings) {
return false;
}

for (const launchSetting of launchSettings) {
try {
const absoluteLaunchSetting = path.join(projectDirectory, launchSetting);
const launchSettingContent = fs.readFileSync(absoluteLaunchSetting);
if (!launchSettingContent) {
continue;
}

if (launchSettingContent.indexOf('"inspectUri"') > 0) {
return true;
}
} catch {
// Swallow IO errors from reading the launchSettings.json files
}
}

return false;
}

function hasBlazorWebAssemblyDebugPrerequisites() {
const jsDebugExtension = vscode.extensions.getExtension('ms-vscode.js-debug-nightly');
if (!jsDebugExtension) {
return false;
}

const debugNodeConfigSection = vscode.workspace.getConfiguration('debug.node');
const useV3NodeValue = debugNodeConfigSection.get('useV3');
if (!useV3NodeValue) {
return false;
}

const debugChromeConfigSection = vscode.workspace.getConfiguration('debug.chrome');
const useV3ChromeValue = debugChromeConfigSection.get('useV3');
if (!useV3ChromeValue) {
return false;
}

return true;
}

function isWebProject(project: MSBuildProject): boolean {
let projectFileText = fs.readFileSync(project.Path, 'utf8');

// Assume that this is an MSBuild project. In that case, look for the 'Sdk="Microsoft.NET.Sdk.Web"' attribute.
// TODO: Have OmniSharp provide the list of SDKs used by a project and check that list instead.
return projectFileText.toLowerCase().indexOf('sdk="microsoft.net.sdk.web"') >= 0;
NTaylorMullen marked this conversation as resolved.
Show resolved Hide resolved
}
Loading