everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
204 lines (202 loc) • 7.32 kB
JavaScript
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