JS Import VS Code Extension

Install selected JS imports in VS Code

When trying snippets of JavaScript from the internet, I often ran into the issue that I didn't have the libraries installed, and so needed to look at each of the imports and turn them into a yarn add or npm install command.

To solve this, I created a VS Code Extension that allows you to highlight some imports, and all the libraries that aren't installed will be installed for you.

I started from the VS Code Extension Template and replaced the hello world function with one called getInstall. To get this to run on right click I needed to add a new key to the contributes key in package.json

"menus": {
    "editor/context": [
        {
            "command": "vscode-js-import-install.install",
            "group": "z_commands"
        }
    ]
}

To get the current selection I first needed to get the active text editor which can be found with vscode.window.activeTextEditor using the default vscode import. From here getting the text of the selection is:

editor.document.getText(editor.selection)

Due to the transition to es modules, I needed to be able to handle both require and import statements. I could have handled each of these separately, but decided to use the rollup bundler to convert the string to commonjs so I would only have require statements. I tried a few different approaches to this, including babel and esbuild, but as none of these are designed for this particular task it was tricky to find good functions that would allow for processing a string, and rollup ended up being the easiest. The function ended up looking like this

async function roll(text) {
  const out = await rollup({
    input: "entry",
    plugins: [virtual({ entry: text })],
  });
  const gen = await out.generate({ format: "cjs" });
  return gen.output[0].code;
}

@rollup/plugin-virtual was used here to allow me to pass a string as rollup is typically used on files

Once I had a consistent data format, I then needed to parse the selection, for this I used acorn along with the acorn-walk package to walk the AST.

walk.simple(acorn.parse(result, { ecmaVersion: 2020 }), {
  CallExpression(node) {
    if (node.callee.name === "require") {
      deps.push(node.arguments[0].value);
    }
  },
});

This goes over the string, looking at all CallExpressions which is all the called expressions in the code, such as require("library"), on each of these, it is checked that the function is named require and the first argument of the function is added to a list of dependencies.

Next is tidying the install to not include currently installed dependencies. This is done by reading the package.json in the project and parsing it to read the dependencies and devDependencies objects.

async function getCurrentDependencies() {
  const doc = await vscode.workspace.openTextDocument(
    vscode.workspace.workspaceFolders[0].uri.path + "/package.json"
  );
  const json = JSON.parse(doc.getText());
  return [
    ...Object.keys(json.dependencies),
    ...Object.keys(json.devDependencies),
  ];
}

I also wanted to exclude reserved names included in node.js such as fs, for this I used the builtin-modules library which gives a list of these names.

Then with these two lists I could filter the list of selected dependencies to find the ones to install

const toInstall = deps.filter(
  (item) =>
    !currentDependencies.includes(item) && !builtinModules.includes(item)
);

Next was finding the package manager to use, for this I decided just to look at npm and yarn as those are what I use, but this method could be expanded to look for things like pnpm. I started with the assumption that the package manager was npm and then read the files in the workspace to see if any were yarn.lock the yarn lockfile, and if so change the package manager to yarn

async function getPackageManager() {
  let packageManager = "npm";
  const dir = await vscode.workspace.fs.readDirectory(
    vscode.workspace.workspaceFolders[0].uri
  );
  const entries = dir.map((item) => item[0]);
  if (entries.includes("yarn.lock")) {
    packageManager = "yarn";
  }
  return packageManager;
}

From there I could create a new terminal and run the install command, making sure to use install for npm and add for yarn

const packageManager = await getPackageManager();
const terminal = vscode.window.createTerminal("Package Installer");
terminal.sendText(
  `${packageManager} ${
    packageManager === "npm" ? "install" : "add"
  } ${toInstall.join(" ")}`
);

Then to ensure this was released to the VS Code marketplace on new GitHub releases I used GitHub actions to run a deploy script on new releases

on:
  release:
    types:
      - created

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 14.x
      - name: Install vsce
        run: npm install -g vsce
      - run: yarn
      - name: Publish
        if: startsWith(github.ref, 'refs/tags/')
        run: yarn run deploy
        env:
          VSCE_PAT: ${{ secrets.VSCE_PAT }}

Where deploy is vsce publish