Created
March 25, 2026 11:57
-
-
Save kolibril13/dc89eb65dea968a15f0c67972fa17d42 to your computer and use it in GitHub Desktop.
Generates a Blender extensions repository index.json by fetching manifests and release assets from configured GitHub repositories.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Generates the static Blender extensions repository JSON during the build by | |
| // fetching the latest release assets and manifests from the configured GitHub repos. | |
| // This is a narrow JavaScript rewrite of Blender's Python | |
| // `subcmd_server.generate()` implementation, which powers the | |
| // `blender --command extension server-generate` command in: | |
| // https://raw.githubusercontent.com/blender/blender/refs/tags/v5.1.0/scripts/addons_core/bl_pkg/cli/blender_ext.py | |
| // | |
| // Unlike Blender's full implementation, this script does not scan local zip files | |
| // or generate HTML. It only reproduces the repository JSON generation step for GitHub Releases based workflow. | |
| import { mkdirSync, writeFileSync } from 'node:fs'; | |
| import { dirname, resolve } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { parse as parseToml } from '@iarna/toml'; | |
| const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | |
| const generatedRepositoryPath = resolve(rootDir, 'public/blender-extensions/index.json'); | |
| const generatedRepositoryDir = dirname(generatedRepositoryPath); | |
| const githubToken = process.env.GITHUB_TOKEN?.trim(); | |
| const extensionSources = [ | |
| { | |
| owner: 'kolibril13', | |
| repo: 'blender_best_presets', | |
| manifestPath: 'blender_manifest.toml' | |
| }, | |
| { | |
| owner: 'kolibril13', | |
| repo: 'blender_csv_import', | |
| manifestPath: 'csv_importer/blender_manifest.toml' | |
| } | |
| ]; | |
| const githubRequestHeaders = { | |
| Accept: 'application/vnd.github+json', | |
| 'User-Agent': 'jan-hendrik-mueller.de/blender-extensions-sync', | |
| ...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}) | |
| }; | |
| function cleanObjectEntries(value) { | |
| return Object.fromEntries( | |
| Object.entries(value).filter(([, entryValue]) => entryValue !== null && entryValue !== undefined) | |
| ); | |
| } | |
| async function fetchJson(url) { | |
| const response = await fetch(url, { headers: githubRequestHeaders }); | |
| if (!response.ok) { | |
| const responseText = (await response.text()).trim(); | |
| const responseSummary = responseText ? ` Response: ${responseText}` : ''; | |
| const authGuidance = | |
| (response.status === 401 || response.status === 403) && !githubToken | |
| ? ' Set GITHUB_TOKEN in your local environment or Cloudflare Pages Variables and Secrets.' | |
| : ''; | |
| throw new Error( | |
| `GitHub API request failed (${response.status}) for ${url}.${authGuidance}${responseSummary}` | |
| ); | |
| } | |
| return response.json(); | |
| } | |
| async function fetchText(url) { | |
| const response = await fetch(url, { headers: githubRequestHeaders }); | |
| if (!response.ok) { | |
| throw new Error(`Request failed (${response.status}) for ${url}`); | |
| } | |
| return response.text(); | |
| } | |
| function normalizeManifest(manifest) { | |
| const buildGenerated = manifest.build?.generated; | |
| const normalizedManifest = { ...manifest }; | |
| if (buildGenerated?.platforms) { | |
| normalizedManifest.platforms = buildGenerated.platforms; | |
| } | |
| if (buildGenerated?.wheels) { | |
| normalizedManifest.wheels = buildGenerated.wheels; | |
| } | |
| return normalizedManifest; | |
| } | |
| function inferPlatformsFromAssetName(assetName) { | |
| const normalizedName = assetName.toLowerCase(); | |
| const inferredPlatforms = []; | |
| if (normalizedName.includes('windows_x64')) { | |
| inferredPlatforms.push('windows-x64'); | |
| } | |
| if (normalizedName.includes('linux_x64')) { | |
| inferredPlatforms.push('linux-x64'); | |
| } | |
| if (normalizedName.includes('macos_arm64')) { | |
| inferredPlatforms.push('macos-arm64'); | |
| } | |
| if (normalizedName.includes('macos_x64')) { | |
| inferredPlatforms.push('macos-x64'); | |
| } | |
| return inferredPlatforms; | |
| } | |
| function buildBaseRepositoryEntry(manifest) { | |
| const { build, wheels, ...manifestData } = normalizeManifest(manifest); | |
| return cleanObjectEntries(manifestData); | |
| } | |
| function buildRepositoryEntry(baseEntry, asset) { | |
| const inferredPlatforms = inferPlatformsFromAssetName(asset.name); | |
| return { | |
| ...baseEntry, | |
| ...(inferredPlatforms.length > 0 ? { platforms: inferredPlatforms } : {}), | |
| archive_url: asset.browser_download_url, | |
| archive_size: asset.size, | |
| archive_hash: asset.digest ?? '' | |
| }; | |
| } | |
| async function loadRepositoryEntriesForSource(source) { | |
| const release = await fetchJson( | |
| `https://api.github.com/repos/${source.owner}/${source.repo}/releases/latest` | |
| ); | |
| const manifestText = await fetchText( | |
| `https://raw.githubusercontent.com/${source.owner}/${source.repo}/${release.tag_name}/${source.manifestPath}` | |
| ); | |
| const manifest = parseToml(manifestText); | |
| const baseEntry = buildBaseRepositoryEntry(manifest); | |
| const zipAssets = release.assets | |
| .filter((asset) => asset.name.endsWith('.zip')) | |
| .sort((left, right) => left.name.localeCompare(right.name)); | |
| if (zipAssets.length === 0) { | |
| throw new Error(`No zip assets found for ${source.owner}/${source.repo}`); | |
| } | |
| return zipAssets.map((asset) => { | |
| console.log(`Using ${asset.name} from ${source.owner}/${source.repo}`); | |
| return buildRepositoryEntry(baseEntry, asset); | |
| }); | |
| } | |
| async function main() { | |
| if (!githubToken) { | |
| console.warn( | |
| 'GITHUB_TOKEN is not set. GitHub API requests may fail in CI or Cloudflare Pages due to anonymous rate limits.' | |
| ); | |
| } | |
| const repositoryData = (await Promise.all(extensionSources.map(loadRepositoryEntriesForSource))) | |
| .flat() | |
| .sort((left, right) => | |
| left.name.localeCompare(right.name) || left.archive_url.localeCompare(right.archive_url) | |
| ); | |
| const generatedRepository = { | |
| version: 'v1', | |
| blocklist: [], | |
| data: repositoryData | |
| }; | |
| mkdirSync(generatedRepositoryDir, { recursive: true }); | |
| writeFileSync(generatedRepositoryPath, `${JSON.stringify(generatedRepository, null, 2)}\n`); | |
| console.log(`Wrote ${generatedRepositoryPath}`); | |
| } | |
| main().catch((error) => { | |
| console.error(error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment