cook.md

Tutorial: build and publish a plugin

This tutorial takes you from an empty folder to a plugin anyone can install from plugins.cook.md. You'll build Copy Ingredients: a button in the recipe preview that copies the recipe's ingredients, at the scale you're viewing, to the clipboard.

Before you start

  • Node.js 20 or newer.
  • Cook Editor, with a recipe folder open.
  • A GitHub account, for signing in to plugins.cook.md.

A Cook Editor plugin is a VS Code extension, so you don't need a Cook Editor source checkout or any special tooling.

Step 1: Create the project

mkdir copy-ingredients
cd copy-ingredients
mkdir src

Add a README.md describing the plugin (it becomes the plugin's page on the marketplace) and a LICENSE file. The packaging tool asks about both if they're missing.

Step 2: Write the manifest

Create package.json. Replace your-namespace with the publisher name you'll register in step 7, and point repository at your own repository.

{
  "name": "copy-ingredients",
  "displayName": "Copy Ingredients",
  "description": "Copy a recipe's ingredients to the clipboard from the recipe preview.",
  "version": "0.1.0",
  "publisher": "your-namespace",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-name/copy-ingredients.git"
  },
  "engines": { "vscode": "^1.100.0" },
  "categories": ["Other"],
  "main": "./out/extension.js",
  "activationEvents": ["onStartupFinished"],
  "contributes": {
    "commands": [
      { "command": "copyIngredients.copy", "title": "Copy Ingredients" }
    ],
    "menus": {
      "cooklang/recipePreview/toolbar": [
        {
          "command": "copyIngredients.copy",
          "when": "cooklangPreviewScheme == file",
          "group": "navigation@20"
        }
      ],
      "commandPalette": [
        { "command": "copyIngredients.copy", "when": "false" }
      ]
    }
  },
  "scripts": {
    "compile": "tsc -p .",
    "vscode:prepublish": "npm run compile",
    "package": "vsce package --no-dependencies",
    "publish:marketplace": "ovsx publish --packagePath copy-ingredients-$npm_package_version.vsix -r https://plugins.cook.md"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/vscode": "~1.100.0",
    "@vscode/vsce": "^3.3.0",
    "ovsx": "^1.0.0",
    "typescript": "~5.4.5"
  }
}

What the parts do:

  • contributes.commands declares the command. It has no icon, so the button shows its title.
  • cooklang/recipePreview/toolbar is an outlet: it puts the command in the recipe preview header. navigation@20 places it before Cook Editor's own Show Source button.
  • cooklangPreviewScheme == file shows the button only for recipes on disk, not for recipes previewed from elsewhere (such as Recipe Hub search results).
  • The commandPalette entry hides the command from the command palette, because it only works when clicked in a preview.
  • @types/vscode is pinned with ~ to the same minor version as engines.vscode. If it's newer, packaging fails.

Then install the tools:

npm install

Step 3: Configure TypeScript

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "out",
    "rootDir": "src",
    "strict": true,
    "sourceMap": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

And .vscodeignore, which keeps sources and build leftovers out of the package:

src/**
tsconfig.json
package-lock.json
.vscodeignore
out/**/*.map
**/.DS_Store

Step 4: Write the code

Create src/extension.ts:

import * as vscode from 'vscode';

// What the recipe preview passes to a toolbar command.
interface PreviewContext {
  version: number;
  path: string;   // relative to the recipe folder; empty for recipes outside it
  scale: number;
}

interface ShoppingListItem {
  name: string;
  quantities: string;
}

interface ShoppingList {
  categories: { name: string; items: ShoppingListItem[] }[];
  other: { name: string; items: ShoppingListItem[] };
}

export async function activate(context: vscode.ExtensionContext): Promise<void> {
  context.subscriptions.push(
    vscode.commands.registerCommand('copyIngredients.copy', copyIngredients),
  );
  try {
    await vscode.commands.executeCommand('cooklang.api.version');
  } catch {
    vscode.window.showWarningMessage('Copy Ingredients needs a newer Cook Editor. Please update it.');
  }
}

async function copyIngredients(ctx?: PreviewContext): Promise<void> {
  if (!ctx?.path) {
    vscode.window.showWarningMessage('Copy Ingredients works on recipes inside your recipe folder.');
    return;
  }
  const list = await vscode.commands.executeCommand<ShoppingList>(
    'cooklang.api.generateShoppingList',
    { recipes: [{ path: ctx.path, scale: ctx.scale }] },
  );
  const lines: string[] = [];
  for (const group of [...list.categories, list.other]) {
    for (const item of group.items) {
      lines.push(item.quantities ? `${item.name}: ${item.quantities}` : item.name);
    }
  }
  await vscode.env.clipboard.writeText(lines.join('\n'));
  vscode.window.showInformationMessage(`Copied ${lines.length} ingredients.`);
}

export function deactivate(): void {}

How it works:

  • When the button is clicked, the preview passes a context with the recipe's path and the scale it's showing. The Outlets page lists every context.
  • cooklang.api.generateShoppingList is part of the Cooklang API. It parses and scales the recipe with the same Rust code Cook Editor uses, and groups ingredients by aisle. Items listed in your pantry are returned separately in pantryItems, so this plugin leaves them out.
  • cooklang.api.version doesn't exist in older Cook Editor versions. Checking it on activation lets you tell the user to update, instead of failing silently later.

Step 5: Build the package

npm run package

This compiles the TypeScript into out/ and creates copy-ingredients-0.1.0.vsix. A .vsix file is the plugin package: the same file you'll test and publish.

Step 6: Try it in Cook Editor

  1. Open the Extensions view from the activity bar on the left.
  2. Open the … menu at the top of the view and choose Install from VSIX…, then pick copy-ingredients-0.1.0.vsix.
  3. Open a recipe from your recipe folder in the preview. Copy Ingredients appears in the header, next to Show Source.
  4. Click it and paste somewhere. Change the servings and try again: the amounts follow the scale.

To try a change, run npm run package again, uninstall the plugin in the Extensions view, and install the new .vsix. Cook Editor won't install a plugin from a VSIX while a copy is already installed. If the old version still seems to be running, restart Cook Editor.

No button? Check that the command id is spelled the same in contributes.commands, in menus and in registerCommand, and that you're looking at the preview, not the recipe source.

Step 7: Get a publishing token

You do this once.

  1. Sign in at plugins.cook.md with GitHub.
  2. Create a personal access token in your dashboard. Keep it secret: anyone with it can publish under your name.
  3. Create your namespace. It must match publisher in package.json:
    npx ovsx create-namespace your-namespace -r https://plugins.cook.md -p <token>

Step 8: Publish

OVSX_PAT=<token> npm run publish:marketplace

This uploads the .vsix you tested in step 6. Check that it's live:

curl https://plugins.cook.md/api/your-namespace/copy-ingredients

Now anyone can find it by searching Copy Ingredients in the Extensions view and clicking Install. Uninstall your VSIX copy first if you want to try the marketplace version yourself.

Step 9: Release updates

npm version patch        # or minor / major; updates package.json
npm run package          # builds copy-ingredients-<new version>.vsix
OVSX_PAT=<token> npm run publish:marketplace

Test the new .vsix as in step 6 before you publish. You can't publish the same version twice, so every release needs a new version number.

Where to go next

  • Outlets: other places to add buttons, such as right-clicking an ingredient in the preview or a recipe in a menu.
  • Cooklang API: recipe references and the shopping list files.
  • Build your first plugin: unit testing, and running a plugin from a Cook Editor source checkout.
  • github.com/cook-md/plugins: the source of the Shopping List, Meal Journal and Recipe Hub plugins, for complete working examples.
  • VS Code Extension API: settings, sidebar views, webviews, status bar items and more.