diff --git a/package.json b/package.json index b2e3ce272..48bd955b7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "install:clean": "yarn workspaces foreach -A run rimraf node_modules && yarn install", "build:orbit": "yarn workspace @litespace/orbit build", "build:types": "yarn workspace @litespace/types build", - "build:atlas": "yarn workspace @litespace/atlas build" + "build:atlas": "yarn workspace @litespace/atlas build", + "vercel": "env-cmd ts-node scripts/vercel.ts" }, "workspaces": { "packages": [ @@ -24,7 +25,12 @@ }, "packageManager": "yarn@4.2.2", "devDependencies": { + "env-cmd": "^10.1.0", "rimraf": "^5.0.7", + "ts-node": "^10.9.2", "vercel": "^34.2.7" + }, + "dependencies": { + "commander": "^12.1.0" } } diff --git a/scripts/vercel.ts b/scripts/vercel.ts new file mode 100644 index 000000000..b2d406e9c --- /dev/null +++ b/scripts/vercel.ts @@ -0,0 +1,142 @@ +import axios from "axios"; +import { Command } from "commander"; +import fs from "node:fs"; +import path from "node:path"; + +type Project = { + id: string; + name: string; + accountId: string; + createdAt: string; + framework: string; + devCommand: string | null; + installCommand: string | null; + buildCommand: string | null; + outputDirectory: string | null; + rootDirectory: string | null; + directoryListing: string | null; + nodeVersion: string | null; +}; + +async function safe(callback: () => Promise): Promise { + try { + return await callback(); + } catch (error: unknown) { + if (axios.isAxiosError(error)) { + const message = error.response?.data?.error?.message || error.message; + return new Error(message); + } + if (error instanceof Error) return error; + throw new Error("Unkown error type"); + } +} + +const client = axios.create({ + baseURL: "https://api.vercel.com", + headers: { Authorization: `Bearer ${process.env.VERCEL_TOKEN}` }, +}); + +async function findProject(name: string): Promise { + return await client + .get(`/v9/projects/${name}`) + .then((response) => response.data); +} + +async function createProject({ + name, + buildCommand = "yarn build", + outputDirectory = "dist", + installCommand = "yarn", +}: { + name: string; + buildCommand?: string; + outputDirectory?: string; + installCommand?: string; +}): Promise { + const { data } = await client.post("/v10/projects", { + name, + buildCommand, + installCommand, + outputDirectory, + framework: "vite", + }); + + return data; +} + +function asVercelDirectory(workspace: string) { + return path.join(workspace, ".vercel"); +} + +function saveProject(vercel: string, project: Project) { + const data = { + projectId: project.id, + orgId: project.accountId, + settings: { + createdAt: project.createdAt, + framework: project.framework, + devCommand: project.devCommand, + installCommand: project.installCommand, + buildCommand: project.buildCommand, + outputDirectory: project.outputDirectory, + rootDirectory: project.rootDirectory, + directoryListing: project.directoryListing, + nodeVersion: project.nodeVersion, + }, + } as const; + + const file = path.join(vercel, "project.json"); + fs.writeFileSync(file, JSON.stringify(data, null, 2)); +} + +const create = new Command() + .name("create") + .argument("", "Project name") + .option("-b, --build ", "Project build command") + .option("-i, --install ", "Project install command") + .option("-o, --output ", "Project output directory") + .action( + async ( + name: string, + options: { build?: string; install?: string; output?: string } + ) => { + const project = await safe(() => + createProject({ + name, + buildCommand: options.build, + installCommand: options.install, + outputDirectory: options.output, + }) + ); + + if (project instanceof Error) throw project; + console.log(`Project (${project.name}) created successfully`); + } + ); + +const pull = new Command() + .name("pull") + .argument("", "Project name") + .argument("", "Where to save project info after pulling it from vercel") + .action(async (name: string, path: string) => { + if (!fs.existsSync(path)) throw new Error(`"${path}" not found`); + + const vercel = asVercelDirectory(path); + if (fs.existsSync(vercel)) + fs.rmSync(vercel, { recursive: true, force: true }); + + fs.mkdirSync(vercel); + + const project = await safe(() => findProject(name)); + if (project instanceof Error) throw project; + + saveProject(vercel, project); + }); + +new Command() + .name("vercel") + .description("Manage Vercel deployments.") + .version("1.0.0") + .addCommand(create) + .addCommand(pull) + .parse(); diff --git a/yarn.lock b/yarn.lock index 4c7dbbe62..da565a310 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11389,6 +11389,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^12.1.0": + version: 12.1.0 + resolution: "commander@npm:12.1.0" + checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -16601,7 +16608,10 @@ __metadata: version: 0.0.0-use.local resolution: "litespace@workspace:." dependencies: + commander: "npm:^12.1.0" + env-cmd: "npm:^10.1.0" rimraf: "npm:^5.0.7" + ts-node: "npm:^10.9.2" vercel: "npm:^34.2.7" languageName: unknown linkType: soft