# Architecture: structure > Describes the codebase structure and shared modules, and how to work with it. > Corresponds to the "Development view" of the [4+1 architectural views](https://en.wikipedia.org/wiki/4%2B1_architectural_view_model). ## Monorepo Structure **The easiest way to open the project for running/debugging:** File > Open Workspace from File > choose `aws-toolkit-vscode/aws-toolkit-vscode.code-workspace` This project is currently set up as a typescript monorepo with the following subprojects: - [`packages/core/`](./packages/core/) - Currently, this package contains almost all of the functionality required for the extension. It was created by moving all of the code from `packages/tookit/` to here. We are currently working on separating non-shareable code out of the core library into the relevant subproject. Running tests for most of the toolkit extension logic occurs in this subproject. - [`packages/toolkit/`](./packages/toolkit/) - Currently, this package is a barebones wrapper that calls activation functions from the core library. As we discover what code cannot be shared across other projects, it will be moved to this package. Running and packaging the extension occurs from this subproject. If you are considering contributing, please consider whether your implementation should live in the core library or in `packages/toolkit`. If your work could be re-used by other packages (e.g. auth mechanisms, utilities), then it may belong in the core library. If instead you are adding something toolkit specific (eg. an integration to a new AWS service in the Explorer Tree), consider putting it in `packages/toolkit`. To import from the core library, please export your desired code using `index.ts` files and add an appropriate `exports` statement in `packages/core/package.json`. Unless otherwise stated, the documentation throughout this project is referring to the code and functionality in `packages/core/` and `packages/toolkit`. Current quirks of the current monorepo status that should be resolved/evaluated in later versions (TODO): - [**Running the test suites in VSCode has changed**](../CONTRIBUTING.md#test) - The [root package.json](../package.json) contains common dependencies for subprojects, and workspace entries for each of the subprojects. - This package contains shortcuts to some of the `npm` scripts found in the subproject(s). - `createRelease` and `newChange` run at the subproject level only, e.g. from root level, try npm run createRelease -w packages/toolkit - To run a script not present in the root `package.json`, use `npm run -w packages/toolkit ` ``` rather than: ```ts // foo.js import 'resources/mycss.css' export function foo() { // some javascript actions } // webview.ts webviewView.webview.html = ` ` ``` ### Testing Currently only manual testing is done. Future work will include setting up some basic unit testing capacity via `JSDOM` and `Vue Testing Library`. Strict type-checking may also be enforced on SFCs; currently the type-checking only exists locally due to gaps in the type definitions for the DOM provided by Vue/TypeScript. ## Prompters A 'prompter' can be thought of as any UI element that displays ('prompts') the user to make some choice or selection, returning their response. This interface is captured with the abstract base class `Prompter` which also contains some extra logic for convenience. Instances of the class can be used alone by calling the async method `prompt`, or by feeding them into a `Wizard`. ```ts const prompter = createInputBox() const response = await prompter.prompt() // Verify that the user did not cancel the prompt if (isValidResponse(response)) { // `response` is now typed as `string` } ``` ### Quick Picks Pickers can be constructed by using the `createQuickPick` factory function. This currently takes two parameters: a collection of 'items' (required), and an object defining additional options. The items can be an array, a Promise for an array, or an `AsyncIterable`. All collections should resolve to the `DataQuickPickItem` interface. Extra configuration options are derived from valid properties on VS Code's `QuickPick` interface, e.g. `title` sets the title of the resulting picker. Some extra options are also present that change or enhance the default behavior of the picker. For example, using `filterBoxInputSettings` causes the picker to create a new quick pick item based off the user's input. #### Items A picker item is simply an extension of VS Code's `QuickPickItem` interface, encapsulating the data it represents in the aptly named `data` field: ```ts // This can be typed as `DataQuickPickItem` const item = { label: 'An item' data: 'some data' } ``` If the user selects this item, then 'some data' should be returned. Note that the type of data (and therefore type of `Prompter`) can largely be inferred; explicit typing, if done at all, should be limited to item definitions: ```ts // Results in `QuickPickPrompter` const prompter = createQuickPick([item]) // Results in `QuickPickPrompter` const prompter = createQuickPick([{ label: 'Another item', data: 0 }]) ``` Often we deal with items derived asychronously (usually by API calls). `createQuickPick` can handle this scenario, showing a loading bar while items load in. For example, consider a scenario where we want to show the user a list of CloudWatch log groups to select. In this case the API is _paginated_, so we should use the `pageableToCollection` utility method to make it easier to map: ```ts interface LogGroup extends CloudWatchLogs.LogGroup { logGroupName: string storedBytes: number } function isValidLogGroup(obj?: CloudWatchLogs.LogGroup): obj is LogGroup { return !!obj && typeof obj.logGroupName === 'string' && typeof obj.storedBytes === 'number' } const requester = (request: CloudWatchLogs.DescribeLogGroupsRequest) => client.invokeDescribeLogGroups(request, sdkClient) const collection = pageableToCollection(requester, request, 'nextToken', 'logGroups') const groupToItem = (group: LogGroup) => ({ label: group.logGroupName, data: group }) const items = collection.flatten().filter(isValidLogGroup).map(groupToItem) const prompter = createQuickPick(items) // Results in `QuickPickPrompter` ``` If we did not care about pagination, we could call the `promise` method on `collection`, causing all items to load in at once: ```ts const items = collection.flatten().filter(isValidLogGroup).map(groupToItem).promise() const prompter = createQuickPick(items) // Results in `QuickPickPrompter` ``` ### Input Box A new input box prompter can be created using the `createInputBox` factory function. Like `createQuickPick`, the input is derived from the properties of VS Code's `InputBox` interface. ### Testing Quick pick prompters can be tested using `createQuickPickTester`, returning an interface that executes actions on the picker. This currently acts on the real VS Code API, meaning the actions happen asynchronously. Very basic example: ```ts const items = [ { label: '1', data: 1 }, { label: '2', data: 2 }, ] const tester = createQuickPickTester(createQuickPick(items)) tester.assertItems(['1', '2']) // Assert that the prompt displays exactly two items with labels '1' and '2'. tester.acceptItem('1') // Accept an item with label '1'. This will fail if no item is found. await tester.result(items[0].data) // Execute the actions, asserting the final result is equivalent to the first item's data ``` ## Wizards Abstractly, a 'wizard' is a collection of discrete, linear steps (subroutines), where each step can potentially be dependent on prior steps, that results in some final state. Wizards are extremely common in top-level flows such as creating a new resource, deployments, or confirmation messages. For these kinds of flows, we have a shared `Wizard` class that handles the bulk of control flow and state management logic for us. ### 1. `Wizard` Class Create a new wizard by extending the base `Wizard` class, using the template type to specify the shape of the wizard state. All wizards have an internal `form` property that is used to assign steps. You can assign UI elements (namely, quickpicks) using the `bindPrompter` method on form elements. This method accepts a callback that should return a `Prompter` given the current state. For this example, we will use `createQuickPick` and `createInputBox` for our prompters: If you need to call async functions to construct your `Wizard` subclass, define your init logic in the `init()` method instead of the constructor. ```ts interface ExampleState { foo: string bar?: number } class ExampleWizard extends Wizard { public constructor() { super() // Note that steps should only be assigned in the constructor by convention // This first step will always be shown as we did not specify any dependencies this.form.foo.bindPrompter(() => createInputBox({ title: 'Enter a string' })) // Our second step is only shown if the length of `foo` is greater than 5 // Because of this, we typed `bar` as potentially being `undefined` in `ExampleState` const items = [ { label: '1', data: 1 }, { label: '2', data: 2 }, ] this.form.bar.bindPrompter((state) => createQuickPick(items, { title: `Select a number (${state.foo})` }), { showWhen: (state) => state.foo?.length > 5, }) } } ``` ### 2. `CompositeWizard` Class `CompositeWizard` extends `Wizard` to create and manage a collection of nested/child wizards. Extend this class to create a wizard that contains other wizards as part of a prompter flow. Use `this.createWizardPrompter()` to use a wizard as a prompter in the `CompositeWizard`. Example: ```ts // Child wizard class ChildWizard extends Wizard {...} // Composite wizard interface SingleNestedWizardForm { ... singleNestedWizardNestedProp: string ... } class SingleNestedWizard extends CompositeWizard { constructor() { super() ... this.form.singleNestedWizardNestedProp.bindPrompter(() => this.createWizardPrompter(ChildWizard) ) ... } } ``` ### Executing Wizards can be ran by calling the async `run` method: ```ts const wizard = new ExampleWizard() const result = await wizard.run() ``` Note that all wizards can potentially return `undefined` if the workflow was cancelled. ### Testing #### Using `WizardTester` Use `createWizardTester` on an instance of a wizard. Tests can then be constructed by asserting both the user-defined and internal state. Using the above `ExampleWizard`: ```ts const tester = await createWizardTester(new ExampleWizard()) tester.foo.assertShowFirst() // Fails if `foo` is not shown (or not shown first) tester.bar.assertDoesNotShow() // True since `foo` is not assigned an explicit value tester.foo.applyInput('Hello, world!') // Manipulate 'user' state tester.bar.assertShow() // True since 'foo' has a defined value ``` #### Using `PrompterTester` Use `PrompterTester` to simulate user behavior (click, input and selection) on prompters to test end-to-end flow of a wizard. Example: ```ts // 1. Register PrompterTester handlers const prompterTester = PrompterTester.init() .handleInputBox('Input Prompter title 1', (inputBox) => { // Register Input Prompter handler inputBox.acceptValue('my-source-bucket-name') }) .handleQuickPick('Quick Pick Prompter title 2', (quickPick) => { // Register Quick Pick Prompter handler // Optional assertion can be added as part of the handler function assert.strictEqual(quickPick.items.length, 2) assert.strictEqual(quickPick.items[0].label, 'Specify required parameters and save as defaults') assert.strictEqual(quickPick.items[1].label, 'Specify required parameters') // Choose item quickPick.acceptItem(quickPick.items[0]) }) .handleQuickPick( 'Quick Pick Prompter with various handler behavior title 3', (() => { // Register handler with dynamic behavior const generator = (function* () { // First call, choose '**' yield async (picker: TestQuickPick) => { await picker.untilReady() assert.strictEqual(picker.items[1].label, '**') picker.acceptItem(picker.items[1]) } // Second call, choose BACK button yield async (picker: TestQuickPick) => { await picker.untilReady() picker.pressButton(vscode.QuickInputButtons.Back) } // Third and subsequent call while (true) { yield async (picker: TestQuickPick) => { await picker.untilReady() picker.acceptItem(picker.items[1]) } } })() return (picker: TestQuickPick) => { const next = generator.next().value return next(picker) } })() ) .build() // 2. Run your wizard class const result = await wizard.run() // 3. Assert your tests prompterTester.assertCallAll() prompterTester.assertCallOrder('Input Prompter title 1', 1) ``` ## Module path debugging Node has an environment variable `NODE_DEBUG=module` that helps to debug module imports. This can be helpful on windows, which can load node modules into uppercase or lower case drive letters, depending on the drive letter of the parent module. You can enable this by adding `"NODE_DEBUG": "module"` into the env of your launch config that you are using. When enabled you can see the file that the import is looking for, the module load request, and the relative file requested. ``` MODULE 88184: looking for ["/aws-toolkit-vscode/packages/core/dist/src"] MODULE 88184: Module._load REQUEST ./codewhisperer/commands/basicCommands parent: /aws-toolkit-vscode/packages/core/dist/src/extension.js MODULE 88184: RELATIVE: requested: ./codewhisperer/commands/basicCommands from parent.id /aws-toolkit-vscode/packages/core/dist/src/extension.js ```