DEV Community

Cover image for VSCode Extensions New Project Release 0.1.0
rxliuli
rxliuli

Posted on

VSCode Extensions New Project Release 0.1.0

Preface

vscode extensions marketplace

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, 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

The final result

demo.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, 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, 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

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

@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
Enter fullscreen mode Exit fullscreen mode

Snipaste_2022-03-29_22-24-32

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

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,
)
Enter fullscreen mode Exit fullscreen mode

Rendering threads

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
Enter fullscreen mode Exit fullscreen mode

Use npm create or npx

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

npx --yes --package create-vite create-vite path --template=react-ts
Enter fullscreen mode Exit fullscreen mode

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

npx --yes --package create-react-app create-react-app path --template=typescript
Enter fullscreen mode Exit fullscreen mode

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

// Other code....
map.set('getState', (key: string) => this.globalState.get(key))
map.set('setState', (key: string, value: any) =>
  this.globalState.update(key, value),
)
Enter fullscreen mode Exit fullscreen mode

Rendering threads

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 }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
rxliuli profile image
rxliuli

I can't seem to get gifs to work, can anyone help me?