Editor extension: marketplace build with the license and no self-updates
node scripts/package.mjs --marketplace builds the .vsix for the Visual Studio Marketplace and Open VSX: diffinery.checkForUpdates off by default, so it doesn't offer diffinery.com's build next to the marketplace's updates. Both builds now ship the repository's LICENSE. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156222tQpRPtP1Em3kEeZnK
- CI / Typecheck and bundlesuccess
- CI / Unit tests (1/2)success
- CI / Unit tests (2/2)success
- End-to-end / End-to-end (agent reviews, tasks, Today)success
- End-to-end / End-to-end (git, PRs, Actions)success
- End-to-end / End-to-end (large pushes)success
- End-to-end / End-to-end (passkeys, registry, email)success
- End-to-end / End-to-end (real histories)success
- End-to-end / End-to-end (runner Docker)success
- End-to-end / End-to-end (security)success
3 files changed+63−24
modifiedapps/vscode/PUBLISHING.md+14−13View file
| @@ -6,16 +6,15 @@ | ||
| 6 | 6 | |
| 7 | 7 | ## Before the first upload |
| 8 | 8 | |
| 9 | -1. Set `"license": "Apache-2.0"` in `package.json` and ship a copy of the repository's `LICENSE` in the package (both | |
| 10 | - registries show it; `vsce` warns without it). | |
| 11 | -2. Create the `codician` publisher on the Marketplace (https://marketplace.visualstudio.com/manage, which needs an | |
| 12 | - Azure DevOps organization for the personal access token) and the `codician` namespace on Open VSX | |
| 13 | - (`npx ovsx create-namespace codician`, after signing the Eclipse publisher agreement). The extension id | |
| 14 | - `codician.diffinery` is what the server's OAuth client (`EDITOR_EXTENSION_ID` in `apps/diffinery/src/auth/oauth.ts`) | |
| 15 | - and the docs use, so keep it. | |
| 16 | -3. Marketplace builds update through the marketplace: ship them with `diffinery.checkForUpdates` defaulting to | |
| 17 | - `false`, or have `src/updates.ts` skip the check when the extension was installed from a gallery, so the two | |
| 18 | - update paths don't fight. | |
| 9 | +1. Create the `codician` publisher on the Marketplace (https://marketplace.visualstudio.com/manage, display name | |
| 10 | + **Codician**; a personal access token for command-line uploads needs an Azure DevOps organization) and the | |
| 11 | + `codician` namespace on Open VSX (`pnpm dlx ovsx create-namespace codician`, after signing the Eclipse publisher | |
| 12 | + agreement). The extension id `codician.diffinery` is what the server's OAuth client (`EDITOR_EXTENSION_ID` in | |
| 13 | + `apps/diffinery/src/auth/oauth.ts`) and the docs use, so keep it. | |
| 14 | +2. The package already carries the license (`"license": "Apache-2.0"` and the repository's `LICENSE` as | |
| 15 | + `extension/LICENSE.txt`), and the marketplace build (`--marketplace` below) ships with `diffinery.checkForUpdates` | |
| 16 | + off by default, so the marketplace's updates and the extension's own check against the Diffinery server don't | |
| 17 | + fight. | |
| 19 | 18 | |
| 20 | 19 | ## Listing |
| 21 | 20 | |
| @@ -33,7 +32,9 @@ | ||
| 33 | 32 | ## Each release |
| 34 | 33 | |
| 35 | 34 | 1. Bump `version` in `package.json` and add a `CHANGELOG.md` entry. |
| 36 | -2. `node scripts/package.mjs --out dist` builds `dist/diffinery-<version>.vsix`, the same file diffinery.com serves. | |
| 37 | -3. `npx @vscode/vsce publish --packagePath dist/diffinery-<version>.vsix` and | |
| 38 | - `npx ovsx publish dist/diffinery-<version>.vsix`, with the tokens from step 2 of *Before the first upload* in `VSCE_PAT` and `OVSX_PAT`. | |
| 35 | +2. `node scripts/package.mjs --out dist --marketplace` builds `dist/diffinery-<version>-marketplace.vsix` | |
| 36 | + (without `--marketplace`, `dist/diffinery-<version>.vsix` is the file diffinery.com serves). | |
| 37 | +3. Upload it on the publisher's page (**New extension → Visual Studio Code**, or **Update** on the extension) and on | |
| 38 | + Open VSX, or from the command line: `pnpm dlx @vscode/vsce publish --packagePath dist/diffinery-<version>-marketplace.vsix` and | |
| 39 | + `pnpm dlx ovsx publish dist/diffinery-<version>-marketplace.vsix`, with the tokens from step 1 of *Before the first upload* in `VSCE_PAT` and `OVSX_PAT`. | |
| 39 | 40 | 4. Deploying Diffinery keeps diffinery.com serving the same version for people who installed from there. |
modifiedapps/vscode/scripts/package.mjs+23−11View file
| @@ -1,7 +1,11 @@ | ||
| 1 | 1 | // Builds the extension and packs it as a .vsix, the file VS Code, Cursor, Windsurf and VSCodium install. |
| 2 | 2 | // |
| 3 | -// node apps/vscode/scripts/package.mjs [--out dir] (writes <dir>/diffinery-<version>.vsix, default apps/vscode/dist) | |
| 3 | +// node apps/vscode/scripts/package.mjs [--out dir] [--marketplace] | |
| 4 | 4 | // |
| 5 | +// writes <dir>/diffinery-<version>.vsix (default dir apps/vscode/dist). --marketplace writes | |
| 6 | +// diffinery-<version>-marketplace.vsix instead, the build for the Visual Studio Marketplace and Open VSX: | |
| 7 | +// the same extension with diffinery.checkForUpdates off by default, because the marketplace updates it. | |
| 8 | +// | |
| 5 | 9 | // apps/diffinery/scripts/build-docs.mjs calls packageExtension() too, so the Worker serves the build at |
| 6 | 10 | // /-/vscode/diffinery.vsix. The .vsix has the layout `vsce package` writes (extension.vsixmanifest, |
| 7 | 11 | // [Content_Types].xml, extension/…), so it can also go to the Visual Studio Marketplace and Open VSX |
| @@ -18,13 +22,18 @@ | ||
| 18 | 22 | |
| 19 | 23 | const xml = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
| 20 | 24 | |
| 21 | -/** The manifest as installed: the workspace package's fields, named for the editor (publisher.name = codician.diffinery). */ | |
| 22 | -function manifestFor(pkg) { | |
| 23 | - const { scripts: _s, devDependencies: _d, dependencies: _deps, private: _p, ...rest } = pkg; | |
| 25 | +/** | |
| 26 | + * The manifest as installed: the workspace package's fields, named for the editor (publisher.name = codician.diffinery). | |
| 27 | + * A marketplace build turns the extension's own update check off by default, so it doesn't offer diffinery.com's | |
| 28 | + * build next to the marketplace's updates. | |
| 29 | + */ | |
| 30 | +export function manifestFor(pkg, { marketplace = false } = {}) { | |
| 31 | + const { scripts: _s, devDependencies: _d, dependencies: _deps, private: _p, ...rest } = structuredClone(pkg); | |
| 32 | + if (marketplace) rest.contributes.configuration.properties["diffinery.checkForUpdates"].default = false; | |
| 24 | 33 | return { ...rest, name: EXTENSION_NAME }; |
| 25 | 34 | } |
| 26 | 35 | |
| 27 | -function vsixManifest(m) { | |
| 36 | +export function vsixManifest(m) { | |
| 28 | 37 | const props = [ |
| 29 | 38 | ["Microsoft.VisualStudio.Code.Engine", m.engines.vscode], |
| 30 | 39 | ["Microsoft.VisualStudio.Code.ExtensionDependencies", (m.extensionDependencies ?? []).join(",")], |
| @@ -62,6 +71,7 @@ | ||
| 62 | 71 | <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" /> |
| 63 | 72 | <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/readme.md" Addressable="true" /> |
| 64 | 73 | <Asset Type="Microsoft.VisualStudio.Services.Content.Changelog" Path="extension/changelog.md" Addressable="true" /> |
| 74 | + <Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" /> | |
| 65 | 75 | <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/${xml(m.icon)}" Addressable="true" /> |
| 66 | 76 | </Assets> |
| 67 | 77 | </PackageManifest> |
| @@ -69,7 +79,7 @@ | ||
| 69 | 79 | } |
| 70 | 80 | |
| 71 | 81 | function contentTypes(names) { |
| 72 | - const types = { ".json": "application/json", ".js": "application/javascript", ".md": "text/markdown", ".png": "image/png", ".woff": "font/woff", ".vsixmanifest": "text/xml" }; | |
| 82 | + const types = { ".json": "application/json", ".js": "application/javascript", ".md": "text/markdown", ".txt": "text/plain", ".png": "image/png", ".woff": "font/woff", ".vsixmanifest": "text/xml" }; | |
| 73 | 83 | const exts = [...new Set(names.map((n) => path.extname(n).toLowerCase()).filter((e) => e && types[e]))].sort(); |
| 74 | 84 | return `<?xml version="1.0" encoding="utf-8"?> |
| 75 | 85 | <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">${exts.map((e) => `<Default Extension="${e}" ContentType="${types[e]}"/>`).join("")}</Types> |
| @@ -146,10 +156,10 @@ | ||
| 146 | 156 | return Buffer.concat([...locals, ...central, end]); |
| 147 | 157 | } |
| 148 | 158 | |
| 149 | -/** Builds and packs the extension. Returns its version and the .vsix bytes. */ | |
| 150 | -export async function packageExtension() { | |
| 159 | +/** Builds and packs the extension (for a marketplace with `marketplace: true`). Returns its version and the .vsix bytes. */ | |
| 160 | +export async function packageExtension({ marketplace = false } = {}) { | |
| 151 | 161 | const pkg = JSON.parse(await readFile(path.join(ROOT, "package.json"), "utf8")); |
| 152 | - const manifest = manifestFor(pkg); | |
| 162 | + const manifest = manifestFor(pkg, { marketplace }); | |
| 153 | 163 | const bundle = await build({ |
| 154 | 164 | entryPoints: [path.join(ROOT, "src/extension.ts")], |
| 155 | 165 | bundle: true, |
| @@ -166,6 +176,7 @@ | ||
| 166 | 176 | ["extension/package.json", JSON.stringify(manifest, null, 2)], |
| 167 | 177 | ["extension/readme.md", await readFile(path.join(ROOT, "README.md"))], |
| 168 | 178 | ["extension/changelog.md", await readFile(path.join(ROOT, "CHANGELOG.md"))], |
| 179 | + ["extension/LICENSE.txt", await readFile(path.join(ROOT, "../../LICENSE"))], | |
| 169 | 180 | ["extension/dist/extension.js", bundle.outputFiles[0].contents], |
| 170 | 181 | [`extension/${manifest.icon}`, await readFile(path.join(ROOT, manifest.icon))], |
| 171 | 182 | ["extension/media/diffinery-icons.woff", await readFile(path.join(ROOT, "media/diffinery-icons.woff"))], |
| @@ -181,9 +192,10 @@ | ||
| 181 | 192 | if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { |
| 182 | 193 | const i = process.argv.indexOf("--out"); |
| 183 | 194 | const out = i > 0 ? path.resolve(process.argv[i + 1]) : path.join(ROOT, "dist"); |
| 184 | - const { version, vsix } = await packageExtension(); | |
| 195 | + const marketplace = process.argv.includes("--marketplace"); | |
| 196 | + const { version, vsix } = await packageExtension({ marketplace }); | |
| 185 | 197 | await mkdir(out, { recursive: true }); |
| 186 | - const file = path.join(out, `${EXTENSION_NAME}-${version}.vsix`); | |
| 198 | + const file = path.join(out, `${EXTENSION_NAME}-${version}${marketplace ? "-marketplace" : ""}.vsix`); | |
| 187 | 199 | await writeFile(file, vsix); |
| 188 | 200 | console.log(`vscode: ${path.relative(process.cwd(), file)} (${(vsix.length / 1024).toFixed(0)} KB)`); |
| 189 | 201 | } |
addedapps/vscode/test/package.test.ts+26−0View file
| @@ -1,0 +1,26 @@ | ||
| 1 | +import { readFileSync } from "node:fs"; | |
| 2 | +import path from "node:path"; | |
| 3 | +import { describe, expect, it } from "vitest"; | |
| 4 | +// @ts-expect-error: a plain .mjs build script without type declarations | |
| 5 | +import { manifestFor, packageExtension, vsixManifest } from "../scripts/package.mjs"; | |
| 6 | + | |
| 7 | +const pkg = JSON.parse(readFileSync(path.join(__dirname, "../package.json"), "utf8")); | |
| 8 | +const updateDefault = (m: typeof pkg): boolean => m.contributes.configuration.properties["diffinery.checkForUpdates"].default; | |
| 9 | + | |
| 10 | +describe("package.mjs", () => { | |
| 11 | + it("keeps the self-update check on for diffinery.com's build and turns it off for marketplaces", () => { | |
| 12 | + expect(updateDefault(manifestFor(pkg))).toBe(true); | |
| 13 | + expect(updateDefault(manifestFor(pkg, { marketplace: true }))).toBe(false); | |
| 14 | + expect(updateDefault(pkg)).toBe(true); | |
| 15 | + expect(manifestFor(pkg, { marketplace: true })).toMatchObject({ name: "diffinery", publisher: "codician", license: "Apache-2.0" }); | |
| 16 | + }); | |
| 17 | + | |
| 18 | + it("ships the license in both builds", async () => { | |
| 19 | + for (const marketplace of [false, true]) { | |
| 20 | + const { vsix } = await packageExtension({ marketplace }); | |
| 21 | + const text = Buffer.from(vsix).toString("latin1"); | |
| 22 | + expect(text).toContain("extension/LICENSE.txt"); | |
| 23 | + } | |
| 24 | + expect(vsixManifest(manifestFor(pkg))).toContain('<Asset Type="Microsoft.VisualStudio.Services.Content.License" Path="extension/LICENSE.txt" Addressable="true" />'); | |
| 25 | + }); | |
| 26 | +}); |