UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

204 lines (202 loc) • 7.32 kB
import { fetchResponse } from "../../utils/http-client.mjs"; import { fetchBosConfigFromFastKv } from "../registry/fastkv.mjs"; import { findConfigPath, isAppDescriptorPath, loadAppDescriptorConfig } from "./load.mjs"; import { getExtendsRef, parseBosRef } from "../../utils/bos-ref.mjs"; import { readWorkspaceCatalog } from "../workspace/catalog.mjs"; import { walkExtendsChain } from "./session.mjs"; import { createWriteStream, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { execa } from "execa"; import { tmpdir } from "node:os"; import { pipeline } from "node:stream/promises"; import { extract } from "tar"; //#region src/services/resolution/init-source.ts async function resolveCatalogChainSource(opts) { const entry = findConfigPath(resolve(opts.sourceDir)) ?? join(resolve(opts.sourceDir), "bos.config.json"); const catalogs = []; const cleanups = []; const resolvedRefs = []; let repository; try { await walkExtendsChain(entry, { env: "production", collectCatalogs: true, registerCleanup: (fn) => cleanups.push(fn), visit: async (link) => { resolvedRefs.unshift(link.ref.startsWith("bos://") ? link.ref : join(link.baseDir, basename(link.ref))); catalogs.push(readWorkspaceCatalog(link.sourceDir ?? link.baseDir)); const repositoryValue = link.config.repository; if (typeof repositoryValue === "string" && repository === void 0) repository = repositoryValue; } }); } finally { for (const cleanup of [...cleanups].reverse()) await cleanup(); } const originRef = opts.originRef ?? (opts.extendsAccount !== void 0 && opts.extendsGateway !== void 0 ? `bos://${opts.extendsAccount}/${opts.extendsGateway}` : void 0); return { catalog: Object.assign({}, ...catalogs), repository, extendsChain: originRef ? [originRef, ...resolvedRefs.slice(1)] : resolvedRefs }; } async function resolveSourceDir(opts) { if (opts.source) { const sourceDir = resolve(opts.source); const configPath = findConfigPath(sourceDir); if (!configPath) throw new Error(`No authored config (bos.app.ts or bos.config.json) found in source directory: ${sourceDir}`); return { sourceDir, parentConfig: isAppDescriptorPath(configPath) ? await loadAppDescriptorConfig(configPath) : JSON.parse(readFileSync(configPath, "utf-8")), cleanup: async () => {} }; } const parentConfig = await fetchParentConfig(opts.extendsAccount, opts.extendsGateway); if (opts.ref) { if (!parentConfig.repository) throw new Error(`Release ${opts.ref} requested but the config at bos://${opts.extendsAccount}/${opts.extendsGateway} names no repository`); const { dir: sourceDir, cleanup, resolvedRef, resolvedSha } = await downloadTarball(parentConfig.repository, opts.ref); return { sourceDir, parentConfig, cleanup, resolvedRef, resolvedSha }; } const chainResult = await resolveRepositoryViaExtendsChain(opts.extendsAccount, opts.extendsGateway); if (chainResult?.repository) { const { dir: sourceDir, cleanup, resolvedRef, resolvedSha } = await downloadTarball(chainResult.repository, opts.ref); return { sourceDir, parentConfig: chainResult.config, cleanup, resolvedRef, resolvedSha }; } return { sourceDir: "", parentConfig, cleanup: async () => {} }; } async function fetchParentConfig(extendsAccount, extendsGateway) { const bosUrl = `bos://${extendsAccount}/${extendsGateway}`; return fetchBosConfigFromFastKv(bosUrl); } async function resolveRepositoryViaExtendsChain(extendsAccount, extendsGateway, visited = /* @__PURE__ */ new Set()) { const key = `bos://${extendsAccount}/${extendsGateway}`; if (visited.has(key)) return null; visited.add(key); try { const config = await fetchParentConfig(extendsAccount, extendsGateway); if (config.repository) return { repository: config.repository, config }; const extendsRef = getExtendsRef(config); if (extendsRef) { const normalized = extendsRef.startsWith("bos://") ? extendsRef : `bos://${extendsRef}`; const parsed = parseBosRef(normalized); if (parsed) { const result = await resolveRepositoryViaExtendsChain(parsed.account, parsed.gateway, visited); if (result) return result; } } return null; } catch { return null; } } /** * The release tarball ref (ADR 0034 pt 4): the canonical `v<version>` tag — * sync's workflow pins and the release script both target it. One ref, one * resolution: a version names exactly one release. */ function releaseRefCandidates(frameworkVersion) { return `v${frameworkVersion}`; } async function downloadTarball(repoUrl, ref) { const parsed = parseGitHubUrl(repoUrl); if (!parsed) throw new Error(`Cannot parse repository URL: ${repoUrl}`); const { owner, repo } = parsed; let response = null; let resolvedRef; const candidates = ref ? Array.isArray(ref) ? [...ref] : [ref] : ["main", "master"]; for (const candidateRef of candidates) { const candidate = await fetchResponse(`https://api.github.com/repos/${owner}/${repo}/tarball/${encodeURIComponent(candidateRef)}`, { headers: { "User-Agent": "everything-dev" }, redirect: "follow", timeout: "60 seconds" }); if (candidate.ok) { response = candidate; resolvedRef = candidateRef; break; } if (candidate.status !== 404) throw new Error(`GitHub tarball download failed: ${candidate.status} ${candidate.statusText}`); } if (!response) throw new Error(ref ? `GitHub tarball download failed for ${repoUrl}: ref not found (tried ${candidates.join(", ")})` : `GitHub tarball download failed for ${repoUrl}: tried main and master`); if (!response.body) throw new Error("GitHub tarball download returned empty body"); const resolvedSha = response.url.match(/\/([0-9a-f]{40})(?:\.tar\.gz)?\b/)?.[1]; const tmpDir = mkTmpDir("bos-init-tarball-"); const tarballPath = join(tmpDir, "source.tar.gz"); const fileStream = createWriteStream(tarballPath); const reader = response.body; await pipeline(reader, fileStream); const extractDir = mkTmpDir("bos-init-extract-"); try { await extract({ cwd: extractDir, file: tarballPath, strip: 1 }); } catch { await execCommand("tar", [ "-xzf", tarballPath, "--strip-components=1", "-C", extractDir ]); } rmSync(tmpDir, { recursive: true, force: true }); return { dir: extractDir, resolvedRef, resolvedSha, cleanup: async () => { rmSync(extractDir, { recursive: true, force: true }); } }; } function parseGitHubUrl(url) { const httpsMatch = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/.*)?$/); if (httpsMatch?.[1] && httpsMatch[2]) return { owner: httpsMatch[1], repo: httpsMatch[2] }; const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/); if (sshMatch?.[1] && sshMatch[2]) return { owner: sshMatch[1], repo: sshMatch[2] }; return null; } function mkTmpDir(prefix) { return mkdtempSync(join(tmpdir(), `${prefix}-`)); } async function execCommand(command, args) { await execa(command, args, { stdio: "pipe", timeout: 12e4 }); } //#endregion export { downloadTarball, fetchParentConfig, parseGitHubUrl, readWorkspaceCatalog, releaseRefCandidates, resolveCatalogChainSource, resolveRepositoryViaExtendsChain, resolveSourceDir }; //# sourceMappingURL=init-source.mjs.map