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

[LaunchConfigManager]: Improve logic for configuring existing launch.json #945

Merged
merged 3 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 85 additions & 18 deletions src/launchConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
SETTINGS_STORE_NAME,
SETTINGS_DEFAULT_URL,
} from './utils';
export type LaunchConfig = 'None' | 'Unsupported' | vscode.DebugConfiguration;
export type LaunchConfig = 'None' | 'Unsupported' | string;
export type CompoundConfig = {
name: string,
configurations: string[],
Expand Down Expand Up @@ -64,6 +64,17 @@ const providedCompoundDebugConfigHeadless: CompoundConfig = {
],
};

export const extensionCompoundConfigs: CompoundConfig[] = [
providedCompoundDebugConfigHeadless,
providedCompoundDebugConfig,
];

export const extensionConfigs: vscode.DebugConfiguration[] = [
providedDebugConfig,
providedHeadlessDebugConfig,
providedLaunchDevToolsConfig,
];

export class LaunchConfigManager {
private launchConfig: LaunchConfig;
private isValidConfig: boolean;
Expand Down Expand Up @@ -102,13 +113,12 @@ export class LaunchConfigManager {
if (fse.pathExistsSync(filePath)) {
// Check if there is a supported debug config
const configs = vscode.workspace.getConfiguration('launch', workspaceUri).get('configurations') as vscode.DebugConfiguration[];
for (const config of configs) {
if (config.type === 'vscode-edge-devtools.debug' || config.type === 'msedge' || config.type === 'edge') {
void vscode.commands.executeCommand('setContext', 'launchJsonStatus', 'Supported');
this.launchConfig = config;
this.isValidConfig = true;
return;
}
const compoundConfigs = vscode.workspace.getConfiguration('launch', workspaceUri).get('compounds') as CompoundConfig[];
if (this.getMissingConfigs(configs, extensionConfigs).length === 0 && this.getMissingConfigs(compoundConfigs, extensionCompoundConfigs).length === 0) {
void vscode.commands.executeCommand('setContext', 'launchJsonStatus', 'Supported');
this.launchConfig = extensionCompoundConfigs[0].name; // extensionCompoundConfigs[0].name => 'Launch Edge Headless and attach DevTools'
this.isValidConfig = true;
return;
}
void vscode.commands.executeCommand('setContext', 'launchJsonStatus', 'Unsupported');
this.launchConfig = 'Unsupported';
Expand Down Expand Up @@ -137,12 +147,24 @@ export class LaunchConfigManager {

// Append a supported debug config to their list of configurations and update workspace configuration
const launchJson = vscode.workspace.getConfiguration('launch', workspaceUri);
const configs = launchJson.get('configurations') as vscode.DebugConfiguration[];
configs.push(providedDebugConfig);
configs.push(providedHeadlessDebugConfig);
configs.push(providedLaunchDevToolsConfig);
let configs = launchJson.get('configurations') as vscode.DebugConfiguration[];
configs = this.replaceDuplicateNameConfigs(configs, extensionConfigs) as vscode.DebugConfiguration[];

const missingConfigs = this.getMissingConfigs(configs, extensionConfigs);
for (const missingConfig of missingConfigs) {
configs.push((missingConfig as vscode.DebugConfiguration));
}
await launchJson.update('configurations', configs) as unknown as Promise<void>;

// Add compound configs
let compounds = launchJson.get('compounds') as CompoundConfig[];
compounds = this.replaceDuplicateNameConfigs(compounds, extensionCompoundConfigs) as CompoundConfig[];
const missingCompoundConfigs = this.getMissingConfigs(compounds, extensionCompoundConfigs);
for (const missingCompoundConfig of missingCompoundConfigs) {
compounds.push((missingCompoundConfig as CompoundConfig));
}
await launchJson.update('compounds', compounds) as unknown as Promise<void>;

// Insert instruction comment
let launchText = fse.readFileSync(workspaceUri.fsPath + relativePath).toString();
const re = /("url":.*startpage[\/\\]+index\.html",)/gm;
Expand All @@ -151,12 +173,6 @@ export class LaunchConfigManager {
launchText = launchText.replace(re, `${match ? match[0] : ''}${instructions}`);
fse.writeFileSync(workspaceUri.fsPath + relativePath, launchText);

// Add compound configs
const compounds = launchJson.get('compounds') as CompoundConfig[];
compounds.push(providedCompoundDebugConfigHeadless);
compounds.push(providedCompoundDebugConfig);
await launchJson.update('compounds', compounds) as unknown as Promise<void>;

// Open launch.json in editor
void vscode.commands.executeCommand('vscode.open', vscode.Uri.joinPath(workspaceUri, relativePath));
this.updateLaunchConfig();
Expand All @@ -165,4 +181,55 @@ export class LaunchConfigManager {
isValidLaunchConfig(): boolean {
return this.isValidConfig;
}

replaceDuplicateNameConfigs(userConfigs: Record<string, unknown>[], extensionConfigs: Record<string, unknown>[]): Record<string, unknown>[] {
const configs = [];
const extensionConfigMap: Map<string, Record<string, unknown>> = new Map();
for (const extensionConfig of extensionConfigs) {
extensionConfigMap.set((extensionConfig.name as string), extensionConfig);
}
for (const userConfig of userConfigs) {
const duplicateNameConfig = extensionConfigMap.get((userConfig.name as string));
const addConfig = duplicateNameConfig ? duplicateNameConfig : userConfig;
configs.push(addConfig);
}
return configs;
}

getMissingConfigs(userConfigs: Record<string, unknown>[], extensionConfigs: Record<string, unknown>[]): Record<string, unknown>[] {
const missingConfigs: Record<string, unknown>[] = [];
for (const extensionConfig of extensionConfigs) {
let configExists = false;
for (const userConfig of userConfigs) {
if (this.compareConfigs(userConfig, extensionConfig)) {
antross marked this conversation as resolved.
Show resolved Hide resolved
configExists = true;
break;
}
}
if (!configExists) {
missingConfigs.push(extensionConfig);
}
}

return missingConfigs;
}

compareConfigs(userConfig: Record<string, unknown>, extensionConfig: Record<string, unknown>): boolean {
for (const property of Object.keys(extensionConfig)) {
if (property === 'url' || property === 'presentation') {
continue;
}
if (Array.isArray(extensionConfig[property]) && Array.isArray(userConfig[property])) {
const userPropertySet = new Set((userConfig[property] as Array<string>));
for (const extensionConfigProperty of (extensionConfig[property] as Array<string>)) {
if (!userPropertySet.has(extensionConfigProperty)) {
return false;
}
}
} else if (userConfig[property] !== extensionConfig[property]) {
return false;
}
}
return true;
}
}
63 changes: 57 additions & 6 deletions test/launchConfigManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import {createFakeVSCode} from "./helpers/helpers";
import { LaunchConfigManager, providedDebugConfig} from "../src/launchConfigManager";
import { extensionCompoundConfigs, extensionConfigs, LaunchConfigManager, providedDebugConfig } from "../src/launchConfigManager";

jest.mock("vscode", () => createFakeVSCode(), { virtual: true });
jest.mock("fs-extra");
Expand Down Expand Up @@ -44,11 +44,17 @@ describe("launchConfigManager", () => {
fse.pathExistsSync.mockImplementation(() => true);
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: (name: string) => [{type: 'vscode-edge-devtools.debug'}]
get: (name: string) => {
if (name === 'configurations') {
return extensionConfigs;
} else {
return extensionCompoundConfigs;
}
}
}
});
const launchConfigManager = LaunchConfigManager.instance;
expect(launchConfigManager.getLaunchConfig()).toEqual({type: 'vscode-edge-devtools.debug'});
expect(launchConfigManager.getLaunchConfig()).toEqual('Launch Edge Headless and attach DevTools');
expect(vscodeMock.commands.executeCommand).toBeCalledWith('setContext', 'launchJsonStatus', 'Supported');
});

Expand All @@ -62,7 +68,7 @@ describe("launchConfigManager", () => {
});

describe('configureLaunchJson', () => {
it('adds a debug config to launch.json', async () => {
it('adds extension configs/compounds to launch.json', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
fse.readFileSync.mockImplementation((() => ''));
Expand All @@ -80,8 +86,9 @@ describe("launchConfigManager", () => {
});
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toBeCalledWith('configurations', expect.arrayContaining([expect.any(Object)]));
await launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toBeCalledWith('configurations', expect.arrayContaining([...extensionConfigs]));
expect(vscodeMock.WorkspaceConfiguration.update).toHaveBeenCalledWith('compounds', expect.arrayContaining([...extensionCompoundConfigs]));
});

it('inserts a comment after the url property', async () => {
Expand All @@ -92,6 +99,50 @@ describe("launchConfigManager", () => {
await launchConfigManager.configureLaunchJson();
expect(fse.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining(expectedText));
});

it('replaces config with duplicate name with extension config', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
fse.readFileSync.mockImplementation((() => ''));
vscodeMock.workspace.workspaceFolders = [{
uri: 'file:///g%3A/GIT/testPage'
}];
vscodeMock.WorkspaceConfiguration = {
update: jest.fn((name: string, value: any) => {}),
};
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: jest.fn((name: string) => [{name: 'Launch Microsoft Edge in headless mode'}]),
update: vscodeMock.WorkspaceConfiguration.update,
}
});
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toBeCalledWith('configurations', Array(3).fill(expect.anything()));
});

it('retains user config', async () => {
const vscodeMock = jest.requireMock("vscode");
const fse = jest.requireMock("fs-extra");
fse.readFileSync.mockImplementation((() => ''));
vscodeMock.workspace.workspaceFolders = [{
uri: 'file:///g%3A/GIT/testPage'
}];
vscodeMock.WorkspaceConfiguration = {
update: jest.fn((name: string, value: any) => {}),
};
vscodeMock.workspace.getConfiguration.mockImplementation(() => {
return {
get: jest.fn((name: string) => [{name: 'Personal config'}]),
update: vscodeMock.WorkspaceConfiguration.update,
}
});
vscodeMock.Uri.joinPath = jest.fn();
const launchConfigManager = LaunchConfigManager.instance;
launchConfigManager.configureLaunchJson();
expect(vscodeMock.WorkspaceConfiguration.update).toBeCalledWith('configurations', Array(4).fill(expect.anything()));
});
});

afterAll(() => {
Expand Down