Skip to content
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
9 changes: 6 additions & 3 deletions client/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function startLanguageServer(
}

// Start a new language server
const command = await getDenoCommandPath();
const command = await getDenoCommandPath(extensionContext.approvedPaths);
if (command == null) {
const message =
"Could not resolve Deno executable. Please ensure it is available " +
Expand Down Expand Up @@ -464,7 +464,7 @@ export function test(
};

assert(workspaceFolder);
const denoCommand = await getDenoCommandName();
const denoCommand = await getDenoCommandName(extensionContext.approvedPaths);
const task = tasks.buildDenoTask(
workspaceFolder,
denoCommand,
Expand Down Expand Up @@ -512,7 +512,10 @@ export function statusBarClicked(
extensionContext.outputChannel.show(true);
if (extensionContext.serverInfo?.upgradeAvailable) {
// Async dispatch on purpose.
denoUpgradePromptAndExecute(extensionContext.serverInfo.upgradeAvailable);
denoUpgradePromptAndExecute(
extensionContext.serverInfo.upgradeAvailable,
extensionContext.approvedPaths,
);
}
};
}
Expand Down
96 changes: 96 additions & 0 deletions client/src/config_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import * as vscode from "vscode";
import { EXTENSION_NS } from "./constants";

const APPROVED_PATHS_KEY = "deno.approvedPaths";

export interface DenoPathInfo {
path: string;
isFromWorkspace: boolean;
}

export function getDenoPathInfo(): DenoPathInfo | undefined {
const config = vscode.workspace.getConfiguration(EXTENSION_NS);
const inspection = config.inspect<string>("path");

const rawPath = config.get<string>("path");
if (typeof rawPath === "string" && rawPath.trim().length > 0) {
// check if path is set in workspace or folder settings (not global/user)
const workspaceValue = inspection?.workspaceValue;
const folderValue = inspection?.workspaceFolderValue;
const isFromWorkspace = (typeof workspaceValue === "string" && workspaceValue.trim().length > 0)
|| (typeof folderValue === "string" && folderValue.trim().length > 0);
return {
path: rawPath.trim(),
isFromWorkspace,
};
} else {
return undefined;
}
}

export class ApprovedConfigPaths {
readonly #context: vscode.ExtensionContext;
readonly #sessionDeniedPaths = new Set<string>();

constructor(context: vscode.ExtensionContext) {
this.#context = context;
}

#getApprovedPaths(): string[] {
const value = this.#context.workspaceState.get(APPROVED_PATHS_KEY);
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is string => typeof item === "string");
}

isPathApproved(path: string): boolean {
const approvedPaths = this.#getApprovedPaths();
return approvedPaths.includes(path);
}

async #approvePath(path: string): Promise<void> {
const approvedPaths = this.#getApprovedPaths();
if (!approvedPaths.includes(path)) {
approvedPaths.push(path);
await this.#context.workspaceState.update(APPROVED_PATHS_KEY, approvedPaths);
}
}

/** Prompts the user for approval if the path hasn't been approved yet. */
async promptForApproval(pathInfo: DenoPathInfo | undefined): Promise<boolean> {
// null and global paths don't need approval
if (pathInfo == null || !pathInfo.isFromWorkspace) {
return true;
}

const path = pathInfo.path;
if (this.isPathApproved(path)) {
return true;
}

// already denied for this session
if (this.#sessionDeniedPaths.has(path)) {
return false;
}

const allow = "Allow";
const deny = "Deny";
const result = await vscode.window.showWarningMessage(
`A workspace setting wants to run a custom Deno executable: ${path}`,
allow,
deny,
);

if (result === allow) {
await this.#approvePath(path);
return true;
}
if (result === deny) {
this.#sessionDeniedPaths.add(path);
}
return false;
}
}
4 changes: 2 additions & 2 deletions client/src/debug_config_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export class DenoDebugConfigurationProvider
program: "${workspaceFolder}/main.ts",
cwd: "${workspaceFolder}",
env: this.#getEnv(),
runtimeExecutable: await getDenoCommandName(),
runtimeExecutable: await getDenoCommandName(this.#extensionContext.approvedPaths),
runtimeArgs: [
"run",
...this.#getAdditionalRuntimeArgs(),
Expand Down Expand Up @@ -99,7 +99,7 @@ export class DenoDebugConfigurationProvider
type: "node",
program: "${file}",
env: this.#getEnv(),
runtimeExecutable: await getDenoCommandName(),
runtimeExecutable: await getDenoCommandName(this.#extensionContext.approvedPaths),
runtimeArgs: [
"run",
...this.#getAdditionalRuntimeArgs(),
Expand Down
3 changes: 3 additions & 0 deletions client/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { ApprovedConfigPaths } from "./config_paths";
import * as commands from "./commands";
import {
ENABLEMENT_FLAG,
Expand Down Expand Up @@ -79,10 +80,12 @@ const extensionContext = {} as DenoExtensionContext;
export async function activate(
context: vscode.ExtensionContext,
): Promise<void> {
extensionContext.approvedPaths = new ApprovedConfigPaths(context);
extensionContext.outputChannel = extensionContext.outputChannel ??
vscode.window.createOutputChannel(LANGUAGE_CLIENT_NAME, { log: true });
extensionContext.denoInfoJson = await getDenoInfoJson(
extensionContext.outputChannel,
extensionContext.approvedPaths,
);
const p2cMap = new Map<string, string>();
extensionContext.clientOptions = {
Expand Down
6 changes: 3 additions & 3 deletions client/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class DenoTaskProvider implements vscode.TaskProvider {

const tasks: vscode.Task[] = [];

const process = await getDenoCommandName();
const process = await getDenoCommandName(this.#extensionContext.approvedPaths);
for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) {
for (const { command, group, problemMatchers } of defs) {
const task = buildDenoTask(
Expand All @@ -159,7 +159,7 @@ class DenoTaskProvider implements vscode.TaskProvider {
if (isWorkspaceFolder(task.scope)) {
return buildDenoTask(
task.scope,
await getDenoCommandName(),
await getDenoCommandName(this.#extensionContext.approvedPaths),
definition,
task.name,
args,
Expand All @@ -170,7 +170,7 @@ class DenoTaskProvider implements vscode.TaskProvider {
if (isWorkspaceFolder(task.scope)) {
return buildDenoConfigTask(
task.scope,
await getDenoCommandName(),
await getDenoCommandName(this.#extensionContext.approvedPaths),
definition.name,
definition.detail,
);
Expand Down
10 changes: 7 additions & 3 deletions client/src/tasks_sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ class DenoTaskProvider implements TaskProvider {
}

async provideTasks(): Promise<Task[]> {
const process = await getDenoCommandName();
const process = await getDenoCommandName(this.#extensionContext.approvedPaths);
const client = this.#extensionContext.client;
const supportsConfigTasks = this.#extensionContext.serverCapabilities
?.experimental?.denoConfigTasks;
Expand Down Expand Up @@ -175,12 +175,15 @@ type TaskTree = Folder[] | DenoJSON[] | NoScripts[];
export class DenoTasksTreeDataProvider implements TreeDataProvider<TreeItem> {
#taskTree: TaskTree | null = null;
#onDidChangeTreeData = new EventEmitter<TreeItem | null>();
#extensionContext: DenoExtensionContext;
readonly onDidChangeTreeData = this.#onDidChangeTreeData.event;

constructor(
public taskProvider: DenoTaskProvider,
subscriptions: ExtensionContext["subscriptions"],
extensionContext: DenoExtensionContext,
) {
this.#extensionContext = extensionContext;
subscriptions.push(
commands.registerCommand("deno.client.runTask", this.#runTask, this),
);
Expand Down Expand Up @@ -239,7 +242,7 @@ export class DenoTasksTreeDataProvider implements TreeDataProvider<TreeItem> {
}
await tasks.executeTask(buildDenoConfigTask(
workspaceFolder,
await getDenoCommandName(),
await getDenoCommandName(this.#extensionContext.approvedPaths),
task.name,
task.command,
sourceUri,
Expand All @@ -251,7 +254,7 @@ export class DenoTasksTreeDataProvider implements TreeDataProvider<TreeItem> {
}

async #debugTask(task: DenoTask) {
const command = `${await getDenoCommandName()} task ${task.task.name}`;
const command = `${await getDenoCommandName(this.#extensionContext.approvedPaths)} task ${task.task.name}`;
commands.executeCommand(
"extension.js-debug.createDebuggerTerminal",
command,
Expand Down Expand Up @@ -381,6 +384,7 @@ export function registerSidebar(
const treeDataProvider = new DenoTasksTreeDataProvider(
taskProvider,
subscriptions,
context,
);

const view = window.createTreeView("denoTasks", {
Expand Down
2 changes: 2 additions & 0 deletions client/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
LanguageClient,
LanguageClientOptions,
} from "vscode-languageclient/node";
import type { ApprovedConfigPaths } from "./config_paths";
import type { DenoServerInfo } from "./server_info";
import type { EnableSettings } from "./shared_types";
import type { DenoStatusBar } from "./status_bar";
Expand All @@ -26,6 +27,7 @@ interface DenoExperimental {
}

export interface DenoExtensionContext {
approvedPaths: ApprovedConfigPaths;
client: LanguageClient | undefined;
clientSubscriptions: { dispose(): unknown }[] | undefined;
clientOptions: LanguageClientOptions;
Expand Down
4 changes: 3 additions & 1 deletion client/src/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { readFileSync } from "fs";
import type { ApprovedConfigPaths } from "./config_paths";
import { EXTENSION_NS } from "./constants";
import * as tasks from "./tasks";
import { UpgradeAvailable } from "./types";
Expand All @@ -11,6 +12,7 @@ import { join } from "path";

export async function denoUpgradePromptAndExecute(
{ latestVersion, isCanary }: UpgradeAvailable,
approvedPaths: ApprovedConfigPaths,
) {
const config = vscode.workspace.getConfiguration(EXTENSION_NS);
let prompt = isCanary
Expand Down Expand Up @@ -70,7 +72,7 @@ export async function denoUpgradePromptAndExecute(
env,
};
assert(workspaceFolder);
const denoCommand = await getDenoCommandName();
const denoCommand = await getDenoCommandName(approvedPaths);
const task = tasks.buildDenoTask(
workspaceFolder,
denoCommand,
Expand Down
35 changes: 17 additions & 18 deletions client/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { EXTENSION_NS } from "./constants";
import { type ApprovedConfigPaths, getDenoPathInfo } from "./config_paths";

import * as fs from "fs";
import * as os from "os";
Expand Down Expand Up @@ -28,13 +28,22 @@ export function assert(cond: unknown, msg = "Assertion failed."): asserts cond {

/** Returns the absolute path to an existing deno command or
* the "deno" command name if not found. */
export async function getDenoCommandName() {
return await getDenoCommandPath() ?? "deno";
export async function getDenoCommandName(approvedPaths: ApprovedConfigPaths) {
return await getDenoCommandPath(approvedPaths) ?? "deno";
}

/** Returns the absolute path to an existing deno command. */
export async function getDenoCommandPath() {
const command = getWorkspaceConfigDenoExePath();
/** Returns the absolute path to an existing deno command.
* Returns undefined if the path is not approved by the user. */
export async function getDenoCommandPath(approvedPaths: ApprovedConfigPaths) {
const pathInfo = getDenoPathInfo();

// check for approval if using a workspace-configured path
const approved = await approvedPaths.promptForApproval(pathInfo);
if (!approved) {
return await getDefaultDenoCommand();
}

const command = pathInfo?.path;
const workspaceFolders = workspace.workspaceFolders;
if (!command || !workspaceFolders) {
return command ?? await getDefaultDenoCommand();
Expand All @@ -52,17 +61,6 @@ export async function getDenoCommandPath() {
}
}

function getWorkspaceConfigDenoExePath() {
const exePath = workspace.getConfiguration(EXTENSION_NS)
.get<string>("path");
// it is possible for the path to be blank. In that case, return undefined
if (typeof exePath === "string" && exePath.trim().length === 0) {
return undefined;
} else {
return exePath;
}
}

async function getDefaultDenoCommand() {
// Adapted from https://github.com/npm/node-which/blob/master/which.js
// Within vscode it will do `require("child_process").spawn("deno")`,
Expand Down Expand Up @@ -117,9 +115,10 @@ async function getDefaultDenoCommand() {

export async function getDenoInfoJson(
outputChannel: vscode.OutputChannel,
approvedPaths: ApprovedConfigPaths,
): Promise<DenoInfoJson | null> {
try {
const command = await getDenoCommandName();
const command = await getDenoCommandName(approvedPaths);
const { stdout, stderr, status, error } = spawnSync(command, [
"info",
"--json",
Expand Down