Skip to content

Commit ab0ac94

Browse files
authored
feat(cloudflare-workers-bindings-extensions): list bindings on the sidebar (#7582)
1 parent 48e7e10 commit ab0ac94

File tree

10 files changed

+794
-210
lines changed

10 files changed

+794
-210
lines changed

.changeset/warm-squids-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cloudflare-workers-bindings-extension": patch
3+
---
4+
5+
Introduce a bindings view that lists all the KV, D1 and R2 bindings on the wrangler config (e.g. `wrangler.toml`, `wrangler.jsonc`)

packages/cloudflare-workers-bindings-extension/.vscode-test.mjs

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/cloudflare-workers-bindings-extension/package.json

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"package": "pnpm run check:type && pnpm run check:lint && node esbuild.js --production",
2020
"compile-tests": "tsc -p . --outDir out",
2121
"watch-tests": "tsc -p . -w --outDir out",
22+
"test": "node ./out/test/runTest.js",
2223
"check:type": "tsc --noEmit",
2324
"check:lint": "eslint src --ext ts",
2425
"build": "vsce package",
@@ -27,46 +28,61 @@
2728
"contributes": {
2829
"commands": [
2930
{
30-
"command": "cloudflare-workers-bindings-extension.testCommand",
31-
"title": "Test command",
32-
"icon": {
33-
"light": "resources/light/edit.svg",
34-
"dark": "resources/dark/edit.svg"
35-
}
31+
"command": "cloudflare-workers-bindings.refresh",
32+
"title": "Cloudflare Workers: Refresh bindings",
33+
"icon": "$(refresh)"
3634
}
3735
],
36+
"menus": {
37+
"view/title": [
38+
{
39+
"command": "cloudflare-workers-bindings.refresh",
40+
"when": "view == cloudflare-workers-bindings",
41+
"group": "navigation"
42+
}
43+
]
44+
},
3845
"views": {
39-
"cloudflare-workers-bindings": [
46+
"cloudflare-workers": [
4047
{
41-
"id": "cloudflare-workers-bindings-extension",
48+
"id": "cloudflare-workers-bindings",
4249
"name": "Bindings",
43-
"icon": "resources/icons/cf-workers-logo.svg"
50+
"icon": "media/cf-workers-logo.svg",
51+
"contextualTitle": "Cloudflare Workers Bindings"
4452
}
4553
]
4654
},
4755
"viewsContainers": {
4856
"activitybar": [
4957
{
50-
"id": "cloudflare-workers-bindings",
58+
"id": "cloudflare-workers",
5159
"title": "Cloudflare Workers",
5260
"icon": "media/cf-workers-logo.svg"
5361
}
5462
]
55-
}
63+
},
64+
"viewsWelcome": [
65+
{
66+
"view": "cloudflare-workers-bindings",
67+
"contents": "Welcome to Cloudflare Workers! [Learn more](https://workers.cloudflare.com).\n[Refresh Bindings](command:cloudflare-workers-bindings.refresh)"
68+
}
69+
]
5670
},
5771
"activationEvents": [
58-
"workspaceContains:{**/wrangler.json,**/wrangler.jsonc,**/wrangler.toml}"
72+
"workspaceContains:**/wrangler.{json,jsonc,toml}"
5973
],
6074
"devDependencies": {
75+
"@types/glob": "^7.1.1",
6176
"@types/mocha": "^10.0.7",
6277
"@types/node": "20.x",
6378
"@types/vscode": "^1.92.0",
6479
"@typescript-eslint/eslint-plugin": "^7.14.1",
6580
"@typescript-eslint/parser": "^7.11.0",
66-
"@vscode/test-cli": "^0.0.9",
67-
"@vscode/test-electron": "^2.4.0",
81+
"@vscode/test-electron": "^2.4.1",
6882
"esbuild": "^0.21.5",
6983
"eslint": "^8.57.0",
84+
"glob": "^7.1.4",
85+
"mocha": "^10.2.0",
7086
"npm-run-all": "^4.1.5",
7187
"typescript": "^5.4.5",
7288
"vsce": "^2.15.0",
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import * as vscode from "vscode";
2+
import { importWrangler } from "./wrangler";
3+
4+
type Config = ReturnType<
5+
ReturnType<typeof importWrangler>["experimental_readRawConfig"]
6+
>["rawConfig"];
7+
8+
type Node =
9+
| {
10+
type: "env";
11+
config: Config;
12+
env: string | null;
13+
}
14+
| {
15+
type: "binding";
16+
config: Config;
17+
env: string | null;
18+
binding: string;
19+
}
20+
| {
21+
type: "resource";
22+
config: Config;
23+
env: string | null;
24+
binding: string;
25+
name: string;
26+
description?: string;
27+
};
28+
29+
export class BindingsProvider implements vscode.TreeDataProvider<Node> {
30+
// Event emitter for refreshing the tree
31+
private _onDidChangeTreeData: vscode.EventEmitter<
32+
Node | undefined | null | void
33+
> = new vscode.EventEmitter<Node | undefined | null | void>();
34+
35+
// To notify the TreeView that the tree data has changed
36+
readonly onDidChangeTreeData: vscode.Event<Node | undefined | null | void> =
37+
this._onDidChangeTreeData.event;
38+
39+
// To manually refresh the tree
40+
refresh(): void {
41+
this._onDidChangeTreeData.fire();
42+
}
43+
44+
getTreeItem(node: Node): vscode.TreeItem {
45+
switch (node.type) {
46+
case "env": {
47+
const item = new vscode.TreeItem(
48+
node.env ?? "Top-level env",
49+
vscode.TreeItemCollapsibleState.Expanded
50+
);
51+
52+
return item;
53+
}
54+
case "binding": {
55+
return new vscode.TreeItem(
56+
node.binding,
57+
vscode.TreeItemCollapsibleState.Expanded
58+
);
59+
}
60+
case "resource": {
61+
const item = new vscode.TreeItem(
62+
node.name,
63+
vscode.TreeItemCollapsibleState.None
64+
);
65+
66+
if (node.description) {
67+
item.description = node.description;
68+
}
69+
70+
return item;
71+
}
72+
}
73+
}
74+
75+
async getChildren(node?: Node): Promise<Node[]> {
76+
if (!node) {
77+
const config = await getWranglerConfig();
78+
79+
if (!config) {
80+
return [];
81+
}
82+
83+
const topLevelEnvNode: Node = {
84+
type: "env",
85+
config,
86+
env: null,
87+
};
88+
const children: Node[] = [];
89+
90+
for (const env of Object.keys(config.env ?? {})) {
91+
const node: Node = {
92+
...topLevelEnvNode,
93+
env,
94+
};
95+
const grandChildren = await this.getChildren(node);
96+
97+
// Include the environment only if it has any bindings
98+
if (grandChildren.length > 0) {
99+
children.push({
100+
...topLevelEnvNode,
101+
env,
102+
});
103+
}
104+
}
105+
106+
const topLevelEnvChildren = await this.getChildren(topLevelEnvNode);
107+
108+
if (children.length > 0) {
109+
// Include top level env only if it has any bindings too
110+
if (topLevelEnvChildren.length > 0) {
111+
children.unshift(topLevelEnvNode);
112+
}
113+
114+
return children;
115+
}
116+
117+
// Skip the top level env if there are no environments
118+
return topLevelEnvChildren;
119+
}
120+
121+
switch (node.type) {
122+
case "env": {
123+
const children: Node[] = [];
124+
const env = node.env ? node.config.env?.[node.env] : node.config;
125+
126+
if (env?.kv_namespaces && env.kv_namespaces.length > 0) {
127+
children.push({
128+
...node,
129+
type: "binding",
130+
binding: "KV Namespaces",
131+
});
132+
}
133+
134+
if (env?.r2_buckets && env.r2_buckets.length > 0) {
135+
children.push({
136+
...node,
137+
type: "binding",
138+
binding: "R2 Buckets",
139+
});
140+
}
141+
142+
if (env?.d1_databases && env.d1_databases.length > 0) {
143+
children.push({
144+
...node,
145+
type: "binding",
146+
binding: "D1 Databases",
147+
});
148+
}
149+
150+
return children;
151+
}
152+
case "binding": {
153+
const children: Node[] = [];
154+
const env = node.env ? node.config.env?.[node.env] : node.config;
155+
156+
switch (node.binding) {
157+
case "KV Namespaces": {
158+
for (const kv of env?.kv_namespaces ?? []) {
159+
children.push({
160+
...node,
161+
type: "resource",
162+
name: kv.binding,
163+
description: kv.id,
164+
});
165+
}
166+
break;
167+
}
168+
case "R2 Buckets": {
169+
for (const r2 of env?.r2_buckets ?? []) {
170+
children.push({
171+
...node,
172+
type: "resource",
173+
name: r2.binding,
174+
description: r2.bucket_name,
175+
});
176+
}
177+
178+
break;
179+
}
180+
case "D1 Databases": {
181+
for (const d1 of env?.d1_databases ?? []) {
182+
children.push({
183+
...node,
184+
type: "resource",
185+
name: d1.binding,
186+
description: d1.database_id,
187+
});
188+
}
189+
break;
190+
}
191+
}
192+
193+
return children;
194+
}
195+
case "resource":
196+
return [];
197+
}
198+
}
199+
}
200+
201+
// Finds the first wrangler config file in the workspace and parse it
202+
export async function getWranglerConfig(): Promise<Config | null> {
203+
const [configUri] = await vscode.workspace.findFiles(
204+
"wrangler.{toml,jsonc,json}",
205+
null,
206+
1
207+
);
208+
209+
if (!configUri) {
210+
return null;
211+
}
212+
213+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(configUri);
214+
215+
if (!workspaceFolder) {
216+
return null;
217+
}
218+
219+
const wrangler = await importWrangler(workspaceFolder.uri.fsPath);
220+
const { rawConfig } = wrangler.experimental_readRawConfig({
221+
config: configUri.fsPath,
222+
});
223+
224+
return rawConfig;
225+
}
Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
import * as vscode from "vscode";
2-
import { importWrangler } from "./wrangler";
2+
import { BindingsProvider } from "./bindings";
33

4-
export async function activate(context: vscode.ExtensionContext) {
5-
vscode.commands.registerCommand(
6-
"cloudflare-workers-bindings-extension.testCommand",
7-
() =>
8-
vscode.window.showInformationMessage(`Successfully called test command.`)
4+
export type Result = {
5+
bindingsProvider: BindingsProvider;
6+
};
7+
8+
export async function activate(
9+
context: vscode.ExtensionContext
10+
): Promise<Result> {
11+
// A tree data provider that returns all the bindings data from the workspace
12+
const bindingsProvider = new BindingsProvider();
13+
// Register the tree view to list bindings
14+
const bindingsView = vscode.window.registerTreeDataProvider(
15+
"cloudflare-workers-bindings",
16+
bindingsProvider
17+
);
18+
19+
// Watch for config file changes
20+
const watcher = vscode.workspace.createFileSystemWatcher(
21+
"**/wrangler.{toml,jsonc,json}"
922
);
1023

11-
const rootPath =
12-
vscode.workspace.workspaceFolders &&
13-
vscode.workspace.workspaceFolders.length > 0
14-
? vscode.workspace.workspaceFolders[0].uri.fsPath
15-
: undefined;
24+
// Refresh the bindings when the wrangler config file changes
25+
watcher.onDidChange(() => bindingsProvider.refresh());
26+
watcher.onDidCreate(() => bindingsProvider.refresh());
27+
watcher.onDidDelete(() => bindingsProvider.refresh());
1628

17-
if (!rootPath) {
18-
return;
19-
}
29+
// Register the refresh command, which is also used by the bindings view
30+
const refreshCommand = vscode.commands.registerCommand(
31+
"cloudflare-workers-bindings.refresh",
32+
() => bindingsProvider.refresh()
33+
);
2034

21-
const wrangler = importWrangler(rootPath);
35+
// Cleanup when the extension is deactivated
36+
context.subscriptions.push(bindingsView, watcher, refreshCommand);
2237

23-
// Do stuff with Wrangler
38+
return {
39+
bindingsProvider,
40+
};
2441
}

0 commit comments

Comments
 (0)