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

feat(cloudflare-workers-bindings-extensions): list bindings on the sidebar #7582

Merged
merged 5 commits into from
Dec 19, 2024
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
5 changes: 5 additions & 0 deletions .changeset/warm-squids-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cloudflare-workers-bindings-extension": patch
---

Introduce a bindings view that lists all the KV, D1 and R2 bindings on the wrangler config (e.g. `wrangler.toml`, `wrangler.jsonc`)

This file was deleted.

44 changes: 30 additions & 14 deletions packages/cloudflare-workers-bindings-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"package": "pnpm run check:type && pnpm run check:lint && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"test": "node ./out/test/runTest.js",
Copy link
Contributor

Choose a reason for hiding this comment

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

We use tsx in various other places for stuff like this. Would that work? I'd alos love to see if it's possible to run this in CI—does it fail if you add a test:ci script?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just tried that but it fails with this message:

Error: Cannot find module '/Users/edmund/Workspace/workers-sdk/packages/cloudflare-workers-bindings-extension/src/test/suite/index'

I have also pushed a commit with the test:ci script. The tests seems to be running fine on windows and macos but fails on ubuntu (https://github.com/cloudflare/workers-sdk/actions/runs/12401086099/job/34619688337). I can look at this next as a follow-up.

Copy link
Member Author

@edmundhung edmundhung Dec 18, 2024

Choose a reason for hiding this comment

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

Looks like we need to have Xvfb enabled specifically for ubuntu:
https://code.visualstudio.com/api/working-with-extensions/continuous-integration#github-actions

"check:type": "tsc --noEmit",
"check:lint": "eslint src --ext ts",
"build": "vsce package",
Expand All @@ -27,46 +28,61 @@
"contributes": {
"commands": [
{
"command": "cloudflare-workers-bindings-extension.testCommand",
"title": "Test command",
"icon": {
"light": "resources/light/edit.svg",
"dark": "resources/dark/edit.svg"
}
"command": "cloudflare-workers-bindings.refresh",
"title": "Cloudflare Workers: Refresh bindings",
"icon": "$(refresh)"
}
],
"menus": {
"view/title": [
{
"command": "cloudflare-workers-bindings.refresh",
"when": "view == cloudflare-workers-bindings",
"group": "navigation"
}
]
},
"views": {
"cloudflare-workers-bindings": [
"cloudflare-workers": [
{
"id": "cloudflare-workers-bindings-extension",
"id": "cloudflare-workers-bindings",
"name": "Bindings",
"icon": "resources/icons/cf-workers-logo.svg"
"icon": "media/cf-workers-logo.svg",
"contextualTitle": "Cloudflare Workers Bindings"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "cloudflare-workers-bindings",
"id": "cloudflare-workers",
"title": "Cloudflare Workers",
"icon": "media/cf-workers-logo.svg"
}
]
}
},
"viewsWelcome": [
{
"view": "cloudflare-workers-bindings",
"contents": "Welcome to Cloudflare Workers! [Learn more](https://workers.cloudflare.com).\n[Refresh Bindings](command:cloudflare-workers-bindings.refresh)"
}
]
},
"activationEvents": [
"workspaceContains:{**/wrangler.json,**/wrangler.jsonc,**/wrangler.toml}"
"workspaceContains:**/wrangler.{json,jsonc,toml}"
],
"devDependencies": {
"@types/glob": "^7.1.1",
"@types/mocha": "^10.0.7",
"@types/node": "20.x",
"@types/vscode": "^1.92.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"@vscode/test-electron": "^2.4.1",
"esbuild": "^0.21.5",
"eslint": "^8.57.0",
"glob": "^7.1.4",
"mocha": "^10.2.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.4.5",
"vsce": "^2.15.0",
Expand Down
225 changes: 225 additions & 0 deletions packages/cloudflare-workers-bindings-extension/src/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import * as vscode from "vscode";
import { importWrangler } from "./wrangler";

type Config = ReturnType<
ReturnType<typeof importWrangler>["experimental_readRawConfig"]
>["rawConfig"];

type Node =
| {
type: "env";
config: Config;
env: string | null;
}
| {
type: "binding";
config: Config;
env: string | null;
binding: string;
}
| {
type: "resource";
config: Config;
env: string | null;
binding: string;
name: string;
description?: string;
};

export class BindingsProvider implements vscode.TreeDataProvider<Node> {
// Event emitter for refreshing the tree
private _onDidChangeTreeData: vscode.EventEmitter<
Node | undefined | null | void
> = new vscode.EventEmitter<Node | undefined | null | void>();

// To notify the TreeView that the tree data has changed
readonly onDidChangeTreeData: vscode.Event<Node | undefined | null | void> =
this._onDidChangeTreeData.event;

// To manually refresh the tree
refresh(): void {
this._onDidChangeTreeData.fire();
}

getTreeItem(node: Node): vscode.TreeItem {
switch (node.type) {
case "env": {
const item = new vscode.TreeItem(
node.env ?? "Top-level env",
vscode.TreeItemCollapsibleState.Expanded
);

return item;
}
case "binding": {
return new vscode.TreeItem(
node.binding,
vscode.TreeItemCollapsibleState.Expanded
);
}
case "resource": {
const item = new vscode.TreeItem(
node.name,
vscode.TreeItemCollapsibleState.None
);

if (node.description) {
item.description = node.description;
}

return item;
}
}
}

async getChildren(node?: Node): Promise<Node[]> {
if (!node) {
const config = await getWranglerConfig();

if (!config) {
return [];
}

const topLevelEnvNode: Node = {
type: "env",
config,
env: null,
};
const children: Node[] = [];

for (const env of Object.keys(config.env ?? {})) {
const node: Node = {
...topLevelEnvNode,
env,
};
const grandChildren = await this.getChildren(node);

// Include the environment only if it has any bindings
if (grandChildren.length > 0) {
children.push({
...topLevelEnvNode,
env,
});
}
}

const topLevelEnvChildren = await this.getChildren(topLevelEnvNode);

if (children.length > 0) {
// Include top level env only if it has any bindings too
if (topLevelEnvChildren.length > 0) {
children.unshift(topLevelEnvNode);
}

return children;
}

// Skip the top level env if there are no environments
return topLevelEnvChildren;
}

switch (node.type) {
case "env": {
const children: Node[] = [];
const env = node.env ? node.config.env?.[node.env] : node.config;

if (env?.kv_namespaces && env.kv_namespaces.length > 0) {
children.push({
...node,
type: "binding",
binding: "KV Namespaces",
});
}

if (env?.r2_buckets && env.r2_buckets.length > 0) {
children.push({
...node,
type: "binding",
binding: "R2 Buckets",
});
}

if (env?.d1_databases && env.d1_databases.length > 0) {
children.push({
...node,
type: "binding",
binding: "D1 Databases",
});
}

return children;
}
case "binding": {
const children: Node[] = [];
const env = node.env ? node.config.env?.[node.env] : node.config;

switch (node.binding) {
case "KV Namespaces": {
for (const kv of env?.kv_namespaces ?? []) {
children.push({
...node,
type: "resource",
name: kv.binding,
description: kv.id,
});
}
break;
}
case "R2 Buckets": {
for (const r2 of env?.r2_buckets ?? []) {
children.push({
...node,
type: "resource",
name: r2.binding,
description: r2.bucket_name,
});
}

break;
}
case "D1 Databases": {
for (const d1 of env?.d1_databases ?? []) {
children.push({
...node,
type: "resource",
name: d1.binding,
description: d1.database_id,
});
}
break;
}
}

return children;
}
case "resource":
return [];
}
}
}

// Finds the first wrangler config file in the workspace and parse it
export async function getWranglerConfig(): Promise<Config | null> {
const [configUri] = await vscode.workspace.findFiles(
"wrangler.{toml,jsonc,json}",
null,
1
);

if (!configUri) {
return null;
}

const workspaceFolder = vscode.workspace.getWorkspaceFolder(configUri);

if (!workspaceFolder) {
return null;
}

const wrangler = await importWrangler(workspaceFolder.uri.fsPath);
const { rawConfig } = wrangler.experimental_readRawConfig({
config: configUri.fsPath,
});

return rawConfig;
}
49 changes: 33 additions & 16 deletions packages/cloudflare-workers-bindings-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
import * as vscode from "vscode";
import { importWrangler } from "./wrangler";
import { BindingsProvider } from "./bindings";

export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(
"cloudflare-workers-bindings-extension.testCommand",
() =>
vscode.window.showInformationMessage(`Successfully called test command.`)
export type Result = {
bindingsProvider: BindingsProvider;
};

export async function activate(
context: vscode.ExtensionContext
): Promise<Result> {
// A tree data provider that returns all the bindings data from the workspace
const bindingsProvider = new BindingsProvider();
// Register the tree view to list bindings
const bindingsView = vscode.window.registerTreeDataProvider(
"cloudflare-workers-bindings",
bindingsProvider
);

// Watch for config file changes
const watcher = vscode.workspace.createFileSystemWatcher(
"**/wrangler.{toml,jsonc,json}"
);

const rootPath =
vscode.workspace.workspaceFolders &&
vscode.workspace.workspaceFolders.length > 0
? vscode.workspace.workspaceFolders[0].uri.fsPath
: undefined;
// Refresh the bindings when the wrangler config file changes
watcher.onDidChange(() => bindingsProvider.refresh());
watcher.onDidCreate(() => bindingsProvider.refresh());
watcher.onDidDelete(() => bindingsProvider.refresh());

if (!rootPath) {
return;
}
// Register the refresh command, which is also used by the bindings view
const refreshCommand = vscode.commands.registerCommand(
"cloudflare-workers-bindings.refresh",
() => bindingsProvider.refresh()
);

const wrangler = importWrangler(rootPath);
// Cleanup when the extension is deactivated
context.subscriptions.push(bindingsView, watcher, refreshCommand);

// Do stuff with Wrangler
return {
bindingsProvider,
};
}
Loading
Loading