UNPKG

o1js

Version:

TypeScript framework for zk-SNARKs and zkApps

361 lines (304 loc) 11.1 kB
import { defineCommand, runMain } from "citty"; import assert from "node:assert"; import path from "path"; import ezSpawn from "@jsdevtools/ez-spawn"; import { createHash } from "node:crypto"; import { hash } from "ohash"; import fsSync from "fs"; import fs from "fs/promises"; import { getPackageManifest, type PackageManifest } from "query-registry"; import type { Comment } from "@pkg-pr-new/utils"; import { abbreviateCommitHash, extractOwnerAndRepo, extractRepository, } from "@pkg-pr-new/utils"; import fg from "fast-glob"; import ignore from "ignore"; import "./environments"; import pkg from "./package.json" with { type: "json" }; import { isBinaryFile } from "isbinaryfile"; import { readPackageJSON, writePackageJSON } from "pkg-types"; import { createDefaultTemplate } from "./template"; declare global { var API_URL: string; } const apiUrl = process.env.API_URL ?? API_URL; const publishUrl = new URL("/publish", apiUrl); const main = defineCommand({ meta: { version: pkg.version, name: "stackblitz", description: "A CLI for pkg.pr.new (Continuous Releases)", }, subCommands: { publish: () => { return { args: { compact: { type: "boolean", description: "compact urls. The shortest form of urls like pkg.pr.new/tinybench@a832a55)", }, pnpm: { type: "boolean", description: "use `pnpm pack` instead of `npm pack --json`", }, template: { type: "string", description: "generate stackblitz templates out of directories in the current repo with the new built packages", }, comment: { type: "string", // "off", "create", "update" (default) description: `"off" for no comments (silent mode). "create" for comment on each publish. "update" for one comment across the pull request with edits on each publish (default)`, default: "update", }, }, run: async ({ args }) => { const paths = (args._.length ? args._ : ["."]) .flatMap((p) => (fg.isDynamicPattern(p) ? fg.sync(p) : p)) .map((p) => path.resolve(p)); const templates = ( typeof args.template === "string" ? [args.template] : ([...(args.template || [])] as string[]) ) .flatMap((p) => (fg.isDynamicPattern(p) ? fg.sync(p) : p)) .map((p) => path.resolve(p)); const formData = new FormData(); const isCompact = !!args.compact; const isPnpm = !!args.pnpm; const comment: Comment = args.comment as Comment; if (!process.env.TEST && process.env.GITHUB_ACTIONS !== "true") { console.error( "Continuous Releases are only available in Github Actions.", ); process.exit(1); } const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_RUN_ATTEMPT, GITHUB_ACTOR_ID, } = process.env; const [owner, repo] = GITHUB_REPOSITORY.split("/"); const metadata = { owner, repo, run: Number(GITHUB_RUN_ID), attempt: Number(GITHUB_RUN_ATTEMPT), actor: Number(GITHUB_ACTOR_ID), }; const key = hash(metadata); const checkResponse = await fetch(new URL("/check", apiUrl), { method: "POST", body: JSON.stringify({ owner, repo, key, }), }); if (!checkResponse.ok) { console.log(await checkResponse.text()); process.exit(1); } const { sha } = await checkResponse.json(); const abbreviatedSha = abbreviateCommitHash(sha); const deps: Map<string, string> = new Map(); for (const p of paths) { const pJsonPath = path.resolve(p, "package.json"); const pJson = await readPackageJSON(pJsonPath); if (!pJson.name) { throw new Error(`"name" field in ${pJsonPath} should be defined`); } if (isCompact) { await verifyCompactMode(pJson.name); } deps.set( pJson.name, new URL( `/${owner}/${repo}/${pJson.name}@${abbreviatedSha}`, apiUrl, ).href, ); } for (const templateDir of templates) { const pJsonPath = path.resolve(templateDir, "package.json"); const pJson = await readPackageJSON(pJsonPath); if (!pJson.name) { throw new Error(`"name" field in ${pJsonPath} should be defined`); } console.log("preparing template:", pJson.name); const restore = await writeDeps(templateDir, deps); const gitignorePath = path.join(templateDir, ".gitignore"); const ig = ignore(); ig.add("node_modules"); if (fsSync.existsSync(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, "utf8"); ig.add(gitignoreContent); } const files = await fg(["**/*"], { cwd: templateDir, dot: true, onlyFiles: true, }); const filteredFiles = files.filter((file) => !ig.ignores(file)); for (const filePath of filteredFiles) { const file = await fs.readFile(path.join(templateDir, filePath)); const isBinary = await isBinaryFile(file); const blob = new Blob([file.buffer], { type: "application/octet-stream", }); formData.append( `template:${pJson.name}:${encodeURIComponent(filePath)}`, isBinary ? blob : await blob.text(), ); } await restore(); } const noDefaultTemplate = args.template === false; if (!templates.length && !noDefaultTemplate) { const project = createDefaultTemplate( Object.fromEntries(deps.entries()), ); for (const filePath of Object.keys(project)) { formData.append( `template:default:${encodeURIComponent(filePath)}`, project[filePath], ); } } const restoreMap = new Map< string, Awaited<ReturnType<typeof writeDeps>> >(); for (const p of paths) { restoreMap.set(p, await writeDeps(p, deps)); } const shasums: Record<string, string> = {}; for (const p of paths) { const pJsonPath = path.resolve(p, "package.json"); try { const pJson = await readPackageJSON(pJsonPath); if (!pJson.name) { throw new Error( `"name" field in ${pJsonPath} should be defined`, ); } const { filename, shasum } = await resolveTarball( isPnpm ? "pnpm" : "npm", p, ); shasums[pJson.name] = shasum; console.log(`shasum for ${pJson.name}(${filename}): ${shasum}`); const file = await fs.readFile(path.resolve(p, filename)); const blob = new Blob([file], { type: "application/octet-stream", }); formData.append(`package:${pJson.name}`, blob, filename); } finally { await restoreMap.get(pJsonPath)?.(); } } const res = await fetch(publishUrl, { method: "POST", headers: { "sb-comment": comment, "sb-compact": `${isCompact}`, "sb-key": key, "sb-shasums": JSON.stringify(shasums), "sb-run-id": GITHUB_RUN_ID, }, body: formData, }); const laterRes = await res.clone().json(); assert.equal( res.status, 200, `publishing failed: ${await res.text()}`, ); console.log("\n"); console.log( `⚡️ Your npm packages are published.\n${[...formData.keys()] .filter((k) => k.startsWith("package:")) .map( (name, i) => `${name.slice("package:".length)}: npm i ${laterRes.urls[i]}`, ) .join("\n")}`, ); }, }; }, link: () => { return { meta: {}, run: () => {}, }; }, }, }); runMain(main); // TODO: we'll add support for yarn if users hit issues with npm async function resolveTarball(pm: "npm" | "pnpm", p: string) { const { stdout } = await ezSpawn.async(`${pm} pack`, { stdio: "overlapped", cwd: p, }); const lines = stdout.split("\n").filter(Boolean); const filename = lines[lines.length - 1].trim(); const shasum = createHash("sha1") .update(await fs.readFile(path.resolve(p, filename))) .digest("hex"); return { filename, shasum }; } async function writeDeps(p: string, deps: Map<string, string>) { const pJsonPath = path.resolve(p, "package.json"); const content = await fs.readFile(pJsonPath, "utf-8"); const pJson = await readPackageJSON(pJsonPath); hijackDeps(deps, pJson.dependencies); hijackDeps(deps, pJson.devDependencies); await writePackageJSON(pJsonPath, pJson); return () => fs.writeFile(pJsonPath, content); } function hijackDeps( newDeps: Map<string, string>, oldDeps?: Record<string, string>, ) { if (!oldDeps) { return; } for (const [newDep, url] of newDeps) { if (newDep in oldDeps) { oldDeps[newDep] = url; } } } async function verifyCompactMode(packageName: string) { let manifest: PackageManifest; try { manifest = await getPackageManifest(packageName); } catch { throw new Error( `pkg-pr-new cannot resolve ${packageName} from npm. --compact flag depends on the package being available in npm. Make sure to have your package on npm first.`, ); } const instruction = `Make sure to configure the 'repository' / 'repository.url' field in its package.json properly. See https://docs.npmjs.com/cli/v10/configuring-npm/package-json#repository for details.`; const repository = extractRepository(manifest); if (!repository) { throw new Error( `pkg-pr-new cannot extract the repository link from the ${packageName} manifest. --compact flag requires the link to be present. ${instruction}`, ); } const match = extractOwnerAndRepo(repository); if (!match) { throw new Error( `pkg-pr-new cannot extract the owner and repo names from the ${packageName} repository link: ${repository}. --compact flag requires these names. ${instruction}`, ); } }