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.

201 lines · 9.5 KBRaw History
1// Builds the extension and packs it as a .vsix, the file VS Code, Cursor, Windsurf and VSCodium install.
2//
3// node apps/vscode/scripts/package.mjs [--out dir] [--marketplace]
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//
9// apps/diffinery/scripts/build-docs.mjs calls packageExtension() too, so the Worker serves the build at
10// /-/vscode/diffinery.vsix. The .vsix has the layout `vsce package` writes (extension.vsixmanifest,
11// [Content_Types].xml, extension/…), so it can also go to the Visual Studio Marketplace and Open VSX
12// unchanged. Packing it here keeps @vscode/vsce and its dependencies out of every CI install. File dates
13// are fixed, so the same sources always give the same bytes (and the same published digest).
14import { build } from "esbuild";
15import { readFile, mkdir, writeFile } from "node:fs/promises";
16import path from "node:path";
17import { fileURLToPath } from "node:url";
18import { deflateRawSync } from "node:zlib";
19
20const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
21const EXTENSION_NAME = "diffinery";
22
23const xml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24
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 */
30export 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;
33 return { ...rest, name: EXTENSION_NAME };
34}
35
36export function vsixManifest(m) {
37 const props = [
38 ["Microsoft.VisualStudio.Code.Engine", m.engines.vscode],
39 ["Microsoft.VisualStudio.Code.ExtensionDependencies", (m.extensionDependencies ?? []).join(",")],
40 ["Microsoft.VisualStudio.Code.ExtensionPack", ""],
41 ["Microsoft.VisualStudio.Code.ExtensionKind", (m.extensionKind ?? ["workspace"]).join(",")],
42 ["Microsoft.VisualStudio.Code.LocalizedLanguages", ""],
43 ["Microsoft.VisualStudio.Services.Links.Source", m.repository.url],
44 ["Microsoft.VisualStudio.Services.Links.Getstarted", m.repository.url],
45 ["Microsoft.VisualStudio.Services.Links.Support", m.bugs.url],
46 ["Microsoft.VisualStudio.Services.Links.Learn", m.homepage],
47 ["Microsoft.VisualStudio.Services.Branding.Color", m.galleryBanner.color],
48 ["Microsoft.VisualStudio.Services.Branding.Theme", m.galleryBanner.theme],
49 ["Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown", "true"],
50 ["Microsoft.VisualStudio.Services.Content.Pricing", "Free"],
51 ];
52 return `<?xml version="1.0" encoding="utf-8"?>
53<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
54 <Metadata>
55 <Identity Language="en-US" Id="${xml(m.name)}" Version="${xml(m.version)}" Publisher="${xml(m.publisher)}" />
56 <DisplayName>${xml(m.displayName)}</DisplayName>
57 <Description xml:space="preserve">${xml(m.description)}</Description>
58 <Tags>${xml((m.keywords ?? []).join(","))}</Tags>
59 <Categories>${xml(m.categories.join(","))}</Categories>
60 <GalleryFlags>Public</GalleryFlags>
61 <Properties>
62${props.map(([k, v]) => ` <Property Id="${k}" Value="${xml(v)}" />`).join("\n")}
63 </Properties>
64 <Icon>extension/${xml(m.icon)}</Icon>
65 </Metadata>
66 <Installation>
67 <InstallationTarget Id="Microsoft.VisualStudio.Code"/>
68 </Installation>
69 <Dependencies/>
70 <Assets>
71 <Asset Type="Microsoft.VisualStudio.Code.Manifest" Path="extension/package.json" Addressable="true" />
72 <Asset Type="Microsoft.VisualStudio.Services.Content.Details" Path="extension/readme.md" Addressable="true" />
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" />
75 <Asset Type="Microsoft.VisualStudio.Services.Icons.Default" Path="extension/${xml(m.icon)}" Addressable="true" />
76 </Assets>
77</PackageManifest>
78`;
79}
80
81function contentTypes(names) {
82 const types = { ".json": "application/json", ".js": "application/javascript", ".md": "text/markdown", ".txt": "text/plain", ".png": "image/png", ".woff": "font/woff", ".vsixmanifest": "text/xml" };
83 const exts = [...new Set(names.map((n) => path.extname(n).toLowerCase()).filter((e) => e && types[e]))].sort();
84 return `<?xml version="1.0" encoding="utf-8"?>
85<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">${exts.map((e) => `<Default Extension="${e}" ContentType="${types[e]}"/>`).join("")}</Types>
86`;
87}
88
89// ---------------------------------------------------------------- a small, deterministic zip writer
90
91const CRC_TABLE = (() => {
92 const t = new Uint32Array(256);
93 for (let n = 0; n < 256; n++) {
94 let c = n;
95 for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
96 t[n] = c >>> 0;
97 }
98 return t;
99})();
100function crc32(buf) {
101 let c = 0xffffffff;
102 for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
103 return (c ^ 0xffffffff) >>> 0;
104}
105// 2026-01-01 00:00 in MS-DOS date/time.
106const DOS_TIME = 0;
107const DOS_DATE = ((2026 - 1980) << 9) | (1 << 5) | 1;
108
109function zip(entries) {
110 const locals = [];
111 const central = [];
112 let offset = 0;
113 for (const { name, data } of entries) {
114 const nameBuf = Buffer.from(name, "utf8");
115 const raw = Buffer.from(data);
116 const deflated = deflateRawSync(raw, { level: 9 });
117 const stored = deflated.length >= raw.length;
118 const body = stored ? raw : deflated;
119 const crc = crc32(raw);
120 const local = Buffer.alloc(30);
121 local.writeUInt32LE(0x04034b50, 0);
122 local.writeUInt16LE(20, 4);
123 local.writeUInt16LE(0x0800, 6); // UTF-8 names
124 local.writeUInt16LE(stored ? 0 : 8, 8);
125 local.writeUInt16LE(DOS_TIME, 10);
126 local.writeUInt16LE(DOS_DATE, 12);
127 local.writeUInt32LE(crc, 14);
128 local.writeUInt32LE(body.length, 18);
129 local.writeUInt32LE(raw.length, 22);
130 local.writeUInt16LE(nameBuf.length, 26);
131 local.writeUInt16LE(0, 28);
132 locals.push(local, nameBuf, body);
133 const cen = Buffer.alloc(46);
134 cen.writeUInt32LE(0x02014b50, 0);
135 cen.writeUInt16LE(20, 4);
136 cen.writeUInt16LE(20, 6);
137 cen.writeUInt16LE(0x0800, 8);
138 cen.writeUInt16LE(stored ? 0 : 8, 10);
139 cen.writeUInt16LE(DOS_TIME, 12);
140 cen.writeUInt16LE(DOS_DATE, 14);
141 cen.writeUInt32LE(crc, 16);
142 cen.writeUInt32LE(body.length, 20);
143 cen.writeUInt32LE(raw.length, 24);
144 cen.writeUInt16LE(nameBuf.length, 28);
145 cen.writeUInt32LE(offset, 42);
146 central.push(cen, nameBuf);
147 offset += 30 + nameBuf.length + body.length;
148 }
149 const cenSize = central.reduce((n, b) => n + b.length, 0);
150 const end = Buffer.alloc(22);
151 end.writeUInt32LE(0x06054b50, 0);
152 end.writeUInt16LE(entries.length, 8);
153 end.writeUInt16LE(entries.length, 10);
154 end.writeUInt32LE(cenSize, 12);
155 end.writeUInt32LE(offset, 16);
156 return Buffer.concat([...locals, ...central, end]);
157}
158
159/** Builds and packs the extension (for a marketplace with `marketplace: true`). Returns its version and the .vsix bytes. */
160export async function packageExtension({ marketplace = false } = {}) {
161 const pkg = JSON.parse(await readFile(path.join(ROOT, "package.json"), "utf8"));
162 const manifest = manifestFor(pkg, { marketplace });
163 const bundle = await build({
164 entryPoints: [path.join(ROOT, "src/extension.ts")],
165 bundle: true,
166 minify: true,
167 platform: "node",
168 target: "node18",
169 format: "cjs",
170 external: ["vscode"],
171 legalComments: "none",
172 write: false,
173 logLevel: "error",
174 });
175 const files = [
176 ["extension/package.json", JSON.stringify(manifest, null, 2)],
177 ["extension/readme.md", await readFile(path.join(ROOT, "README.md"))],
178 ["extension/changelog.md", await readFile(path.join(ROOT, "CHANGELOG.md"))],
179 ["extension/LICENSE.txt", await readFile(path.join(ROOT, "../../LICENSE"))],
180 ["extension/dist/extension.js", bundle.outputFiles[0].contents],
181 [`extension/${manifest.icon}`, await readFile(path.join(ROOT, manifest.icon))],
182 ["extension/media/diffinery-icons.woff", await readFile(path.join(ROOT, "media/diffinery-icons.woff"))],
183 ];
184 const entries = [
185 { name: "extension.vsixmanifest", data: vsixManifest(manifest) },
186 { name: "[Content_Types].xml", data: contentTypes(["extension.vsixmanifest", ...files.map(([n]) => n)]) },
187 ...files.map(([name, data]) => ({ name, data })),
188 ];
189 return { version: manifest.version, vsix: zip(entries) };
190}
191
192if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
193 const i = process.argv.indexOf("--out");
194 const out = i > 0 ? path.resolve(process.argv[i + 1]) : path.join(ROOT, "dist");
195 const marketplace = process.argv.includes("--marketplace");
196 const { version, vsix } = await packageExtension({ marketplace });
197 await mkdir(out, { recursive: true });
198 const file = path.join(out, `${EXTENSION_NAME}-${version}${marketplace ? "-marketplace" : ""}.vsix`);
199 await writeFile(file, vsix);
200 console.log(`vscode: ${path.relative(process.cwd(), file)} (${(vsix.length / 1024).toFixed(0)} KB)`);
201}