Skip to content

Commit

Permalink
refactor: code cleanup, separate class w/ activate() deactivate() lif…
Browse files Browse the repository at this point in the history
…ecycle implementations
  • Loading branch information
tinahollygb committed Oct 6, 2022
1 parent 6bd865f commit 0cf88bc
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 57 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.vscode-test/
*.vsix
.eslintcache
.idea
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v16.14.2
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"scripts": {
"vscode:prepublish": "yarn run compile",
"build": "yarn run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"pretest": "yarn run compile && yarn run lint",
Expand Down
5 changes: 3 additions & 2 deletions src/api/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export interface IApiClient {
type ApiClientConfig = {
featuresHost: string;
appHost: string;
apiKey: string;
// apiKey: string;
featuresKey: string;
};

export enum ApiError {
Expand All @@ -33,7 +34,7 @@ export class ApiClient implements IApiClient {

getFeatures = async () => {
return this.featuresClient
.get<FeaturesResponse>(`/api/features/${this.options.apiKey}`)
.get<FeaturesResponse>(`/api/features/${this.options.featuresKey}`)
.then((response) => {
const features: FeatureDefinition[] = [];

Expand Down
57 changes: 8 additions & 49 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,14 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from "vscode";
import { ApiClient } from "./api/api-client";
import { FeatureListTreeDataProvider } from "./features/FeatureListTreeDataProvider";
import { getWorkspaceRootPath } from "./utils/vscode-utils";
import { ExtensionInitialization } from "./services/extension-initialization.service";

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export async function activate(context: vscode.ExtensionContext) {
// Register the tree view
const rootPath = getWorkspaceRootPath();

if (rootPath) {
// TODO: Read config from local file
const apiClient = new ApiClient({
appHost: "http://localhost:3100",
featuresHost: "http://localhost:3100",
apiKey: "key_prod_b118a91f4800c2c6",
});
const features = await apiClient.getFeatures();

vscode.window.registerTreeDataProvider(
"featuresList",
new FeatureListTreeDataProvider(context, features)
);

const treeView = vscode.window.createTreeView("featuresList", {
treeDataProvider: new FeatureListTreeDataProvider(context, features),
});
let extensionInitializationService: ExtensionInitialization | null = null;

context.subscriptions.push(treeView);
}

// Use the console to output diagnostic information (console.log) and errors (console.error)
// This line of code will only be executed once when your extension is activated
console.log("Congratulations, your extension 'growthbook' is now active!");

// The command has been defined in the package.json file
// Now provide the implementation of the command with registerCommand
// The commandId parameter must match the command field in package.json
const disposable = vscode.commands.registerCommand(
"growthbook.helloWorld",
() => {
// The code you place here will be executed every time your command is executed
// Display a message box to the user
vscode.window.showInformationMessage("Hello World from growthbook!");
}
);
export async function activate(context: vscode.ExtensionContext) {
extensionInitializationService = ExtensionInitialization.getInstance(context);

context.subscriptions.push(disposable);
extensionInitializationService?.activate();
}

// This method is called when your extension is deactivated
export function deactivate() {}
export function deactivate() {
extensionInitializationService?.deactivate();
}
4 changes: 2 additions & 2 deletions src/features/FeatureListTreeDataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Fs from "fs";
import * as Path from "path";
import {
Event,
Expand Down Expand Up @@ -33,6 +32,7 @@ export class FeatureListTreeDataProvider

// TODO: Do we need this element??
getChildren(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
element?: FeatureListTreeItem | undefined
): ProviderResult<FeatureListTreeItem[]> {
const treeItems = this.features.map(
Expand All @@ -56,7 +56,7 @@ export class FeatureListTreeDataProvider
// }
}

class FeatureListTreeItem extends TreeItem {
export class FeatureListTreeItem extends TreeItem {
constructor(
public readonly label: string,
private feature: FeatureDefinition,
Expand Down
108 changes: 108 additions & 0 deletions src/services/extension-initialization.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as vscode from "vscode";
import { ApiClient } from "../api/api-client";
import {
FeatureListTreeDataProvider,
FeatureListTreeItem,
} from "../features/FeatureListTreeDataProvider";
import { FeatureDefinition } from "../features/types";
import {
getGrowthBookConfig,
getWorkspaceRootPath,
GrowthBookConfig,
} from "../utils/vscode-utils";

interface IExtensionInitialization {
activate(): Promise<void>;
deactivate(): Promise<void>;
}

let extensionInitializationService: ExtensionInitialization | null = null;

/**
* Use the getInstance() method of the class.
* If you do not provide a valid config, your instance will be null.
*/
export class ExtensionInitialization implements IExtensionInitialization {
private apiClient: ApiClient;
private features: FeatureDefinition[] = [];
private treeView: vscode.TreeView<FeatureListTreeItem> | null = null;

private constructor(
private context: vscode.ExtensionContext,
private growthBookConfig: GrowthBookConfig
) {
const { featuresHost, featuresKey, appHost } = this.growthBookConfig;

/* eslint-disable @typescript-eslint/no-non-null-assertion -- handled in getInstance() */
this.apiClient = new ApiClient({
appHost: appHost!,
featuresHost: featuresHost!,
featuresKey: featuresKey!,
});
/* eslint-enable @typescript-eslint/no-non-null-assertion */
}

static getInstance(
context: vscode.ExtensionContext,
growthBookConfig: GrowthBookConfig | null = getGrowthBookConfig(
getWorkspaceRootPath() || ""
)
): ExtensionInitialization | null {
if (!growthBookConfig) {
console.error("ImplementationError: missing GrowthBook config");
return null;
}

const { featuresHost, featuresKey, appHost } = growthBookConfig;
if (!featuresHost || !featuresKey || !appHost) {
console.error(
"ImplementationError: all GrowthBook config values are required for initialization"
);
return null;
}

extensionInitializationService = new ExtensionInitialization(
context,
growthBookConfig
);

return extensionInitializationService;
}

/**
* Implement in the extension's activate() lifecycle hook
*/
async activate(): Promise<void> {
this.features = await this.apiClient.getFeatures();
this.initializeTreeView();

return Promise.resolve();
}

/**
* Implement in the extension's deactivate() lifecycle hook
*/
async deactivate(): Promise<void> {
this.treeView?.dispose();
return Promise.resolve();
}

/**
* Populate the left side tree view with the features data
*/
private initializeTreeView(): void {
vscode.window.registerTreeDataProvider(
"featuresList",
new FeatureListTreeDataProvider(this.context, this.features)
);

this.treeView = vscode.window.createTreeView("featuresList", {
treeDataProvider: new FeatureListTreeDataProvider(
this.context,
this.features
),
});

this.context.subscriptions.push(this.treeView);
}
}
32 changes: 32 additions & 0 deletions src/utils/vscode-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,35 @@ export const doesPathExist = (path: string): boolean => {
}
return true;
};

export type GrowthBookConfig = {
featuresHost: string | null;
featuresKey: string | null;
appHost: string | null;
};

/**
* Get the GrowthBook config from the provided workspace root.
* If no file is found, or the file fails to parse, it will return null.
* @param workspaceRoot
* @returns
*/
export const getGrowthBookConfig = (
workspaceRoot: string
): GrowthBookConfig | null => {
const configPath = `${workspaceRoot}/.growthbook.json`;

try {
const configContents = Fs.readFileSync(configPath, "utf-8");
console.log("config contents", configContents);
const parsedConfig = JSON.parse(configContents);

return {
featuresHost: parsedConfig.featuresHost || null,
featuresKey: parsedConfig.featuresKey || null,
appHost: parsedConfig.appHost || null,
};
} catch (e) {
return null;
}
};
7 changes: 3 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}

0 comments on commit 0cf88bc

Please sign in to comment.