Skip to content

Commit 35d6e14

Browse files
authored
feat: prompt for custom workspace "deno.path" setting (#1356)
1 parent ce6efe8 commit 35d6e14

File tree

9 files changed

+139
-30
lines changed

9 files changed

+139
-30
lines changed

client/src/commands.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function startLanguageServer(
119119
}
120120

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

466466
assert(workspaceFolder);
467-
const denoCommand = await getDenoCommandName();
467+
const denoCommand = await getDenoCommandName(extensionContext.approvedPaths);
468468
const task = tasks.buildDenoTask(
469469
workspaceFolder,
470470
denoCommand,
@@ -512,7 +512,10 @@ export function statusBarClicked(
512512
extensionContext.outputChannel.show(true);
513513
if (extensionContext.serverInfo?.upgradeAvailable) {
514514
// Async dispatch on purpose.
515-
denoUpgradePromptAndExecute(extensionContext.serverInfo.upgradeAvailable);
515+
denoUpgradePromptAndExecute(
516+
extensionContext.serverInfo.upgradeAvailable,
517+
extensionContext.approvedPaths,
518+
);
516519
}
517520
};
518521
}

client/src/config_paths.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
2+
3+
import * as vscode from "vscode";
4+
import { EXTENSION_NS } from "./constants";
5+
6+
const APPROVED_PATHS_KEY = "deno.approvedPaths";
7+
8+
export interface DenoPathInfo {
9+
path: string;
10+
isFromWorkspace: boolean;
11+
}
12+
13+
export function getDenoPathInfo(): DenoPathInfo | undefined {
14+
const config = vscode.workspace.getConfiguration(EXTENSION_NS);
15+
const inspection = config.inspect<string>("path");
16+
17+
const rawPath = config.get<string>("path");
18+
if (typeof rawPath === "string" && rawPath.trim().length > 0) {
19+
// check if path is set in workspace or folder settings (not global/user)
20+
const workspaceValue = inspection?.workspaceValue;
21+
const folderValue = inspection?.workspaceFolderValue;
22+
const isFromWorkspace = (typeof workspaceValue === "string" && workspaceValue.trim().length > 0)
23+
|| (typeof folderValue === "string" && folderValue.trim().length > 0);
24+
return {
25+
path: rawPath.trim(),
26+
isFromWorkspace,
27+
};
28+
} else {
29+
return undefined;
30+
}
31+
}
32+
33+
export class ApprovedConfigPaths {
34+
readonly #context: vscode.ExtensionContext;
35+
readonly #sessionDeniedPaths = new Set<string>();
36+
37+
constructor(context: vscode.ExtensionContext) {
38+
this.#context = context;
39+
}
40+
41+
#getApprovedPaths(): string[] {
42+
const value = this.#context.workspaceState.get(APPROVED_PATHS_KEY);
43+
if (!Array.isArray(value)) {
44+
return [];
45+
}
46+
return value.filter((item): item is string => typeof item === "string");
47+
}
48+
49+
isPathApproved(path: string): boolean {
50+
const approvedPaths = this.#getApprovedPaths();
51+
return approvedPaths.includes(path);
52+
}
53+
54+
async #approvePath(path: string): Promise<void> {
55+
const approvedPaths = this.#getApprovedPaths();
56+
if (!approvedPaths.includes(path)) {
57+
approvedPaths.push(path);
58+
await this.#context.workspaceState.update(APPROVED_PATHS_KEY, approvedPaths);
59+
}
60+
}
61+
62+
/** Prompts the user for approval if the path hasn't been approved yet. */
63+
async promptForApproval(pathInfo: DenoPathInfo | undefined): Promise<boolean> {
64+
// null and global paths don't need approval
65+
if (pathInfo == null || !pathInfo.isFromWorkspace) {
66+
return true;
67+
}
68+
69+
const path = pathInfo.path;
70+
if (this.isPathApproved(path)) {
71+
return true;
72+
}
73+
74+
// already denied for this session
75+
if (this.#sessionDeniedPaths.has(path)) {
76+
return false;
77+
}
78+
79+
const allow = "Allow";
80+
const deny = "Deny";
81+
const result = await vscode.window.showWarningMessage(
82+
`A workspace setting wants to run a custom Deno executable: ${path}`,
83+
allow,
84+
deny,
85+
);
86+
87+
if (result === allow) {
88+
await this.#approvePath(path);
89+
return true;
90+
}
91+
if (result === deny) {
92+
this.#sessionDeniedPaths.add(path);
93+
}
94+
return false;
95+
}
96+
}

client/src/debug_config_provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class DenoDebugConfigurationProvider
6262
program: "${workspaceFolder}/main.ts",
6363
cwd: "${workspaceFolder}",
6464
env: this.#getEnv(),
65-
runtimeExecutable: await getDenoCommandName(),
65+
runtimeExecutable: await getDenoCommandName(this.#extensionContext.approvedPaths),
6666
runtimeArgs: [
6767
"run",
6868
...this.#getAdditionalRuntimeArgs(),
@@ -99,7 +99,7 @@ export class DenoDebugConfigurationProvider
9999
type: "node",
100100
program: "${file}",
101101
env: this.#getEnv(),
102-
runtimeExecutable: await getDenoCommandName(),
102+
runtimeExecutable: await getDenoCommandName(this.#extensionContext.approvedPaths),
103103
runtimeArgs: [
104104
"run",
105105
...this.#getAdditionalRuntimeArgs(),

client/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
22

3+
import { ApprovedConfigPaths } from "./config_paths";
34
import * as commands from "./commands";
45
import {
56
ENABLEMENT_FLAG,
@@ -79,10 +80,12 @@ const extensionContext = {} as DenoExtensionContext;
7980
export async function activate(
8081
context: vscode.ExtensionContext,
8182
): Promise<void> {
83+
extensionContext.approvedPaths = new ApprovedConfigPaths(context);
8284
extensionContext.outputChannel = extensionContext.outputChannel ??
8385
vscode.window.createOutputChannel(LANGUAGE_CLIENT_NAME, { log: true });
8486
extensionContext.denoInfoJson = await getDenoInfoJson(
8587
extensionContext.outputChannel,
88+
extensionContext.approvedPaths,
8689
);
8790
const p2cMap = new Map<string, string>();
8891
extensionContext.clientOptions = {

client/src/tasks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ class DenoTaskProvider implements vscode.TaskProvider {
132132

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

135-
const process = await getDenoCommandName();
135+
const process = await getDenoCommandName(this.#extensionContext.approvedPaths);
136136
for (const workspaceFolder of vscode.workspace.workspaceFolders ?? []) {
137137
for (const { command, group, problemMatchers } of defs) {
138138
const task = buildDenoTask(
@@ -159,7 +159,7 @@ class DenoTaskProvider implements vscode.TaskProvider {
159159
if (isWorkspaceFolder(task.scope)) {
160160
return buildDenoTask(
161161
task.scope,
162-
await getDenoCommandName(),
162+
await getDenoCommandName(this.#extensionContext.approvedPaths),
163163
definition,
164164
task.name,
165165
args,
@@ -170,7 +170,7 @@ class DenoTaskProvider implements vscode.TaskProvider {
170170
if (isWorkspaceFolder(task.scope)) {
171171
return buildDenoConfigTask(
172172
task.scope,
173-
await getDenoCommandName(),
173+
await getDenoCommandName(this.#extensionContext.approvedPaths),
174174
definition.name,
175175
definition.detail,
176176
);

client/src/tasks_sidebar.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class DenoTaskProvider implements TaskProvider {
122122
}
123123

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

180181
constructor(
181182
public taskProvider: DenoTaskProvider,
182183
subscriptions: ExtensionContext["subscriptions"],
184+
extensionContext: DenoExtensionContext,
183185
) {
186+
this.#extensionContext = extensionContext;
184187
subscriptions.push(
185188
commands.registerCommand("deno.client.runTask", this.#runTask, this),
186189
);
@@ -239,7 +242,7 @@ export class DenoTasksTreeDataProvider implements TreeDataProvider<TreeItem> {
239242
}
240243
await tasks.executeTask(buildDenoConfigTask(
241244
workspaceFolder,
242-
await getDenoCommandName(),
245+
await getDenoCommandName(this.#extensionContext.approvedPaths),
243246
task.name,
244247
task.command,
245248
sourceUri,
@@ -251,7 +254,7 @@ export class DenoTasksTreeDataProvider implements TreeDataProvider<TreeItem> {
251254
}
252255

253256
async #debugTask(task: DenoTask) {
254-
const command = `${await getDenoCommandName()} task ${task.task.name}`;
257+
const command = `${await getDenoCommandName(this.#extensionContext.approvedPaths)} task ${task.task.name}`;
255258
commands.executeCommand(
256259
"extension.js-debug.createDebuggerTerminal",
257260
command,
@@ -381,6 +384,7 @@ export function registerSidebar(
381384
const treeDataProvider = new DenoTasksTreeDataProvider(
382385
taskProvider,
383386
subscriptions,
387+
context,
384388
);
385389

386390
const view = window.createTreeView("denoTasks", {

client/src/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
LanguageClient,
55
LanguageClientOptions,
66
} from "vscode-languageclient/node";
7+
import type { ApprovedConfigPaths } from "./config_paths";
78
import type { DenoServerInfo } from "./server_info";
89
import type { EnableSettings } from "./shared_types";
910
import type { DenoStatusBar } from "./status_bar";
@@ -26,6 +27,7 @@ interface DenoExperimental {
2627
}
2728

2829
export interface DenoExtensionContext {
30+
approvedPaths: ApprovedConfigPaths;
2931
client: LanguageClient | undefined;
3032
clientSubscriptions: { dispose(): unknown }[] | undefined;
3133
clientOptions: LanguageClientOptions;

client/src/upgrade.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
22

33
import { readFileSync } from "fs";
4+
import type { ApprovedConfigPaths } from "./config_paths";
45
import { EXTENSION_NS } from "./constants";
56
import * as tasks from "./tasks";
67
import { UpgradeAvailable } from "./types";
@@ -11,6 +12,7 @@ import { join } from "path";
1112

1213
export async function denoUpgradePromptAndExecute(
1314
{ latestVersion, isCanary }: UpgradeAvailable,
15+
approvedPaths: ApprovedConfigPaths,
1416
) {
1517
const config = vscode.workspace.getConfiguration(EXTENSION_NS);
1618
let prompt = isCanary
@@ -70,7 +72,7 @@ export async function denoUpgradePromptAndExecute(
7072
env,
7173
};
7274
assert(workspaceFolder);
73-
const denoCommand = await getDenoCommandName();
75+
const denoCommand = await getDenoCommandName(approvedPaths);
7476
const task = tasks.buildDenoTask(
7577
workspaceFolder,
7678
denoCommand,

client/src/util.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
22

3-
import { EXTENSION_NS } from "./constants";
3+
import { type ApprovedConfigPaths, getDenoPathInfo } from "./config_paths";
44

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

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

35-
/** Returns the absolute path to an existing deno command. */
36-
export async function getDenoCommandPath() {
37-
const command = getWorkspaceConfigDenoExePath();
35+
/** Returns the absolute path to an existing deno command.
36+
* Returns undefined if the path is not approved by the user. */
37+
export async function getDenoCommandPath(approvedPaths: ApprovedConfigPaths) {
38+
const pathInfo = getDenoPathInfo();
39+
40+
// check for approval if using a workspace-configured path
41+
const approved = await approvedPaths.promptForApproval(pathInfo);
42+
if (!approved) {
43+
return await getDefaultDenoCommand();
44+
}
45+
46+
const command = pathInfo?.path;
3847
const workspaceFolders = workspace.workspaceFolders;
3948
if (!command || !workspaceFolders) {
4049
return command ?? await getDefaultDenoCommand();
@@ -52,17 +61,6 @@ export async function getDenoCommandPath() {
5261
}
5362
}
5463

55-
function getWorkspaceConfigDenoExePath() {
56-
const exePath = workspace.getConfiguration(EXTENSION_NS)
57-
.get<string>("path");
58-
// it is possible for the path to be blank. In that case, return undefined
59-
if (typeof exePath === "string" && exePath.trim().length === 0) {
60-
return undefined;
61-
} else {
62-
return exePath;
63-
}
64-
}
65-
6664
async function getDefaultDenoCommand() {
6765
// Adapted from https://github.com/npm/node-which/blob/master/which.js
6866
// Within vscode it will do `require("child_process").spawn("deno")`,
@@ -117,9 +115,10 @@ async function getDefaultDenoCommand() {
117115

118116
export async function getDenoInfoJson(
119117
outputChannel: vscode.OutputChannel,
118+
approvedPaths: ApprovedConfigPaths,
120119
): Promise<DenoInfoJson | null> {
121120
try {
122-
const command = await getDenoCommandName();
121+
const command = await getDenoCommandName(approvedPaths);
123122
const { stdout, stderr, status, error } = spawnSync(command, [
124123
"info",
125124
"--json",

0 commit comments

Comments
 (0)