VSCode Extensions New Project Release 0.1.0
Tags: vscode, tooling, webdev
Published: 2022-03-30 00:53:11

## Preface

> [vscode extensions marketplace](https://marketplace.visualstudio.com/items?itemName=rxliuli.new-project)

This idea started a few weeks after I switched from webstorm to vscode, and this past weekend I thought, why does the Jetbrains IDE have a [project creation bootstrap](https://www.jetbrains.com/help/idea/create-new-project-twitter-bootstrap.html), but vscode doesn't support it, and you can only use the command line tool? With this in mind, I decided to implement the equivalent plugin in vscode myself.

> I have also developed a plugin for webstorm to generate vite projects [Vite Integrated](https://plugins.jetbrains.com/plugin/16897/)

The final result

[demo.gif](https://blog.rxliuli.com/resource/6ac71ef7d19d409c8e7cef995c396876.gif)

## Ideas

The basic idea is to create a project by rendering the configuration via vscode webview and then stitching the npx command in the main thread to execute it. Specifically, from the user's point of view, there are several steps

1. open the project creation panel via the command or the menu on the ui
2. select the generator to use
3. select the location of the generated project
4. select or enter some configuration required by the generator
5. Create the project
6. if it is a subdirectory of the current project, open package.json in the directory, otherwise open a new window

## Some problems encountered

- How to use react to develop the view layer
- How to keep the webview interface style consistent with vscode
- How to communicate with the main thread for the rendering layer
- Using npm create or npx
- How to persist state

## How to develop a view layer using react

I initially wanted to deploy the webview view layer as a standalone on github pages, and the vscode plugin part only contains bundles. vscode requires html strings as the entry point by default, which made things a bit complicated, but luckily I found the official example project [hello-world-vite](https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/react/hello-world-vite), which makes integrating our familiar technology stack a bit easier, and the project is modified from this template.

## How to keep the webview interface style consistent with vscode

This problem was a bit complicated earlier, but with the release of the official vscode ui library [vscode-webview-ui-toolkit](https://github.com/microsoft/vscode-webview-ui-toolkit), and the recent release of the default integration with react, the problem has become much simpler -- not perfect, of course.

The official diagram

![toolkit-artwork](https://blog.rxliuli.com/resource/4d90e30cfde74c23bef01991a8ea261b.png)

It provides some basic components

- `badge`
- `button`
- `checkbox`
- `data-grid`
- `divider`
- `dropdown`
- `link`
- `option`
- `panels`
- `progress-ring`
- `radio`
- `radio-group`
- `tag`
- `text-area`
- `text-field`

However, I still found some problems when using it in practice, mainly including

- The type definition of components is not accurate, resulting in a serious need to rely on storybook's documentation and dream back to JavaScript
- Some components are not intuitive, for example, `panels/select/text-field` is a little different from the general practice, `panels` tab/panel has to be written twice, `select` does not separate `value/label`, `text-field` uses
- The ui library doesn't support tree shaking, resulting in a relatively large bundle and a project footprint of **439.98kb/74.79%**

The dependency tree is really terrible, that's why I didn't replace react => preact

```sh
@vscode/codicons 0.0.29
@vscode/webview-ui-toolkit 0.9.3
├── @microsoft/fast-element 1.8.0
├─┬ @microsoft/fast-foundation 2.37.2
│ ├── @microsoft/fast-element 1.8.0
│ ├── @microsoft/fast-web-utilities 5.1.0
│ ├── tabbable 5.2.1
│ └── tslib 1.14.1
├─┬ @microsoft/fast-react-wrapper 0.1.43
│ ├── @microsoft/fast-element 1.8.0
│ ├─┬ @microsoft/fast-foundation 2.37.2
│ │ ├── @microsoft/fast-element 1.8.0
│ │ ├── @microsoft/fast-web-utilities 5.1.0
│ │ ├── tabbable 5.2.1
│ │ └── tslib 1.14.1
│ └── react 17.0.2 peer
└── react 17.0.2 peer
```

![Snipaste_2022-03-29_22-24-32](https://blog.rxliuli.com/resource/8cb9112697704e6699908d0537c647fe.png)

## How to implement the rendering layer to communicate with the main thread

Since the core is implemented through `postMessage/onmessage`, it is very similar to web worker or iframe communication. Since the code is relatively simple, I did not encapsulate the complex communication logic.

1. the basic data structure of communication is defined
2. use map in the main thread part to save the function exposed by `channel => handle` to the rendering thread, execute the corresponding method after receiving the ready-made message from the rendering and throw it to the rendering thread via postMessage
3. simply wrap it in the rendering thread so that the call is made by passing `channel + data` to call the method exposed in the main thread and get the result asynchronously

Key Code

Main thread

```ts
const map = new Map<string, (. . args: any[]) => any>()
map.set('hello', (name: string) => `hello ${name}`)
webview.onDidReceiveMessage(
  async (message: any) => {
    const { command, data = [], callback } = message
    if (!map.has(command)) {
      throw new Error(`Command not found ${command}`)
    }
    const res = await map.get(command)! (... .data)
    if (callback) {
      console.log('callback: ', callback)
      this._panel.webview.postMessage({
        command: callback,
        data: [res],
      })
    }
  },
  undefined,
  this._disposables,
)
```

Rendering threads

```ts
import type { WebviewApi } from 'vscode-webview'

class VSCodeAPIWrapper {
  private readonly vsCodeApi: WebviewApi<unknown> | undefined

  constructor() {
    if (typeof acquireVsCodeApi === 'function') {
      this.vsCodeApi = acquireVsCodeApi()
    }
  }

  public postMessage(message: unknown) {
    if (this.vsCodeApi) {
      this.vsCodeApi.postMessage(message)
    } else {
      console.log(message)
    }
  }

  async invoke(options: {
    command: string
    default?: any
    args?: any[]
  }): Promise<any> {
    return await new Promise<string>((resolve) => {
      if (typeof acquireVsCodeApi! == 'function') {
        resolve(options.default)
        return
      }
      const id = Date.now() + '_' + Math.random()
      const listener = (message: MessageEvent) => {
        const data = message.data
        if (data.command === id) {
          resolve(data.data[0])
          window.removeEventListener('message', listener)
        }
      }
      window.addEventListener('message', listener)
      vscode.postMessage({
        command: options.command,
        data: options.args,
        callback: id,
      })
    })
  }
}

export const vscode = new VSCodeAPIWrapper()

console.log(await vscode.invoke({ command: 'world' })) // hello world
```

## Use npm create or npx

For example, the command to create a react project using vite is

```sh
npx --yes --package create-vite create-vite path --template=react-ts
```

The command to create a project using create-react-app is

```sh
npx --yes --package create-react-app create-react-app path --template=typescript
```

Some people may ask: "Why don't you use a command like `npm create` to create a project?"
At first glance, it seems that the `create` command is already supported by the major package managers `npm/yarn/pnpm`, but it has a few limitations

1. npm package names are restricted in that the part that does not contain an organization name must start with `create-`, but in fact not all cli's start with `create-`, e.g. `@angular/cli`
2. `pnpm create` is not equivalent to npm and needs to be adapted to each package manager separately
3. webstorm still uses npx for project creation, which influenced my choice

So it's only natural to choose npx, and we may support global configuration using `pnpx` in the future.

## How to persist state

vscode actually provides an official solution to implement `WebviewPanelSerializer` that allows `acquireVsCodeApi()`'s `getState/setState` to be persisted, but it has some problems that caused me not to use it in the end.

1. the state is restored only when the webview is rendered for the first time, but the changes are not persisted until vscode is closed, i.e. if you need to create two projects, the first time you create some configuration items, but the second time the configuration items are not cached, and they only take effect when vscode is reopened, which is really strange (or maybe I'm using the wrong one)
2. the interface does not support kv storage, it only supports getting the full state or setting the full state

So I choose to use `context.globalState` to persist data

main thread

```ts
// Other code....
map.set('getState', (key: string) => this.globalState.get(key))
map.set('setState', (key: string, value: any) =>
  this.globalState.update(key, value),
)
```

Rendering threads

```ts
import type { WebviewApi } from 'vscode-webview'

class VSCodeAPIWrapper {
  // Other code...
  async getState(key: string): Promise<unknown | undefined> {
    if (this.vsCodeApi) {
      return await this.invoke({ command: 'getState', args: [key] })
    } else {
      const state = localStorage.getItem(key)
      return state ? JSON.parse(state) : undefined
    }
  }

  async setState<T extends unknown | undefined>(
    key: string,
    newState: T,
  ): Promise<void> {
    if (this.vsCodeApi) {
      return await this.invoke({ command: 'setState', args: [key, newState] })
    } else {
      localStorage.setItem(key, JSON.stringify(newState))
    }
  }
}

export const vscode = new VSCodeAPIWrapper()

const store = (await vscode.getState(props.id)) as { location: string }
```

It is now possible to support that selected generators and configuration items will still be cached the next time they are used, which is really handy when you need to create projects with the same generator, especially if there are more generator options.

## Conclusion

vscode is still not as good as webstorm in some ways, but its popularity is simply unstoppable, and many front-end projects even officially support only vscode editors, which makes it a no-option at some point (e.g. vue3, nx). So instead of complaining about it, you should try to make it better.