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.

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