Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

Editor extension: marketplace build with the license and no self-updates #311

Mergedtest-botagenton behalf of cnbrkdmrcimerged into main from marketplace-builds
3 files changed+63−24base 2b6234c → head 7bc0788
modifiedapps/vscode/PUBLISHING.md+14−13View file
@@ -6,16 +6,15 @@
66
77 ## Before the first upload
88
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.
1918
2019 ## Listing
2120
@@ -33,7 +32,9 @@
3332 ## Each release
3433
3534 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`.
3940 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 @@
11 // Builds the extension and packs it as a .vsix, the file VS Code, Cursor, Windsurf and VSCodium install.
22 //
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]
44 //
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+//
59 // apps/diffinery/scripts/build-docs.mjs calls packageExtension() too, so the Worker serves the build at
610 // /-/vscode/diffinery.vsix. The .vsix has the layout `vsce package` writes (extension.vsixmanifest,
711 // [Content_Types].xml, extension/…), so it can also go to the Visual Studio Marketplace and Open VSX
@@ -18,13 +22,18 @@
1822
1923 const xml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2024
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;
2433 return { ...rest, name: EXTENSION_NAME };
2534 }
2635
27-function vsixManifest(m) {
36+export function vsixManifest(m) {
2837 const props = [
2938 ["Microsoft.VisualStudio.Code.Engine", m.engines.vscode],
3039 ["Microsoft.VisualStudio.Code.ExtensionDependencies", (m.extensionDependencies ?? []).join(",")],
@@ -62,6 +71,7 @@
6271 <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
6372 <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/readme.md" Addressable="true" />
6473 <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" />
6575 <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/${xml(m.icon)}" Addressable="true" />
6676 </Assets>
6777 </PackageManifest>
@@ -69,7 +79,7 @@
6979 }
7080
7181 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" };
7383 const exts = [...new Set(names.map((n) => path.extname(n).toLowerCase()).filter((e) => e && types[e]))].sort();
7484 return `<?xml version="1.0" encoding="utf-8"?>
7585 <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 @@
146156 return Buffer.concat([...locals, ...central, end]);
147157 }
148158
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 } = {}) {
151161 const pkg = JSON.parse(await readFile(path.join(ROOT, "package.json"), "utf8"));
152- const manifest = manifestFor(pkg);
162+ const manifest = manifestFor(pkg, { marketplace });
153163 const bundle = await build({
154164 entryPoints: [path.join(ROOT, "src/extension.ts")],
155165 bundle: true,
@@ -166,6 +176,7 @@
166176 ["extension/package.json", JSON.stringify(manifest, null, 2)],
167177 ["extension/readme.md", await readFile(path.join(ROOT, "README.md"))],
168178 ["extension/changelog.md", await readFile(path.join(ROOT, "CHANGELOG.md"))],
179+ ["extension/LICENSE.txt", await readFile(path.join(ROOT, "../../LICENSE"))],
169180 ["extension/dist/extension.js", bundle.outputFiles[0].contents],
170181 [`extension/${manifest.icon}`, await readFile(path.join(ROOT, manifest.icon))],
171182 ["extension/media/diffinery-icons.woff", await readFile(path.join(ROOT, "media/diffinery-icons.woff"))],
@@ -181,9 +192,10 @@
181192 if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
182193 const i = process.argv.indexOf("--out");
183194 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 });
185197 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`);
187199 await writeFile(file, vsix);
188200 console.log(`vscode: ${path.relative(process.cwd(), file)} (${(vsix.length / 1024).toFixed(0)} KB)`);
189201 }
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+});