Improve MineGeo interface and functionality

This commit is contained in:
2026-10-06 19:24:41 +05:00
parent 7404f1dc1c
commit 233e4d57f0
67 changed files with 3053 additions and 643 deletions

306
scripts/deploy-vercel.mjs Normal file
View File

@@ -0,0 +1,306 @@
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { execFileSync } from "node:child_process";
import { unzipSync } from "fflate";
const defaultRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
export const deploymentProject = {
name: "minegeo",
project: "prj_St6KMC5H8PakQxe3UCQxJa5b8gWy",
teamId: "team_03U5cmiAfXJvpuxDDh33CqOT",
productionUrl: "https://minegeo.vercel.app",
};
export function deploymentFiles(root = defaultRoot) {
const files = [];
const rootReal = fs.realpathSync(root);
let privateTerms = [];
try {
const remote = execFileSync(
"git",
["-C", root, "remote", "get-url", "origin"],
{
encoding: "utf8",
windowsHide: true,
stdio: ["ignore", "pipe", "ignore"],
},
).trim();
const hostname = remote.includes("://")
? new URL(remote).hostname
: remote.split("@").pop().split(":")[0];
privateTerms = [remote, hostname]
.filter(Boolean)
.map((value) => value.toLowerCase());
} catch {
/* A source snapshot can be deployed without Git. */
}
function add(full, relative) {
const real = fs.realpathSync(full);
if (real !== rootReal && !real.startsWith(rootReal + path.sep))
throw new Error(`Deployment file escapes the project: ${relative}`);
let data = fs.readFileSync(full);
if (relative === "next.config.ts")
data = Buffer.from(
data.toString().replace(/^\s*allowedDevOrigins:.*\r?\n/gm, ""),
);
if (relative === "package.json") {
const pkg = JSON.parse(data);
pkg.scripts = {
prebuild: "node scripts/clean-build.mjs",
build: "next build",
start: "next start",
};
delete pkg.repository;
delete pkg.bugs;
delete pkg.homepage;
data = Buffer.from(JSON.stringify(pkg, null, 2));
}
if (relative === "tsconfig.json") {
const config = JSON.parse(data);
config.include = [
"next-env.d.ts",
"next.config.ts",
"proxy.ts",
"app/**/*.ts",
"app/**/*.tsx",
"components/**/*.tsx",
"lib/**/*.ts",
"shared/**/*.ts",
".next/types/**/*.ts",
];
data = Buffer.from(JSON.stringify(config, null, 2));
}
const contents = relative.endsWith(".jar")
? Object.values(unzipSync(data))
: [data];
if (
contents.some((content) =>
privateTerms.some((term) =>
Buffer.from(content).toString("utf8").toLowerCase().includes(term),
),
)
)
throw new Error(
`Private Git server reference found in ${relative}. Remove it before deploying.`,
);
files.push({
file: relative,
data: data.toString("base64"),
encoding: "base64",
});
}
function walk(relative) {
const dir = path.join(root, relative);
if (!fs.existsSync(dir)) return;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (
entry.name.startsWith(".") ||
entry.name === "node_modules" ||
(relative === "public" &&
["generated", "downloads", "artifacts", "build"].includes(entry.name))
)
continue;
const file = path.posix.join(relative, entry.name);
if (entry.isDirectory()) walk(file);
else add(path.join(root, file), file);
}
}
for (const directory of ["app", "components", "lib", "shared", "public"])
walk(directory);
for (const file of [
"package.json",
"package-lock.json",
"tsconfig.json",
"next.config.ts",
"next-env.d.ts",
"proxy.ts",
"postcss.config.mjs",
"vercel.json",
"scripts/clean-build.mjs",
]) {
if (fs.existsSync(path.join(root, file))) add(path.join(root, file), file);
}
if (
!files.some((file) => file.file === "package.json") ||
!files.some((file) => file.file === "app/layout.tsx")
)
throw new Error("Run this command from a complete MineGeo project.");
return files;
}
export function deploymentPayload(files, preview = false) {
return {
name: deploymentProject.name,
project: process.env.VERCEL_PROJECT_ID || deploymentProject.project,
...(preview ? {} : { target: "production" }),
files,
projectSettings: {
framework: "nextjs",
nodeVersion: "22.x",
buildCommand: "npm run build",
skipGitConnectDuringLink: true,
},
};
}
function accessToken() {
if (process.env.VERCEL_TOKEN) return process.env.VERCEL_TOKEN;
const authPaths =
process.platform === "win32"
? [
path.join(
process.env.APPDATA || path.join(os.homedir(), "AppData/Roaming"),
"com.vercel.cli/Data/auth.json",
),
]
: [
path.join(
process.env.XDG_DATA_HOME ||
path.join(os.homedir(), ".local/share"),
"com.vercel.cli/auth.json",
),
path.join(
os.homedir(),
"Library/Application Support/com.vercel.cli/auth.json",
),
];
for (const file of authPaths) {
try {
const token = JSON.parse(fs.readFileSync(file, "utf8")).token;
if (typeof token === "string") return token;
} catch {
/* No CLI login saved here. */
}
}
throw new Error(
"Vercel authentication is needed once: run vercel login, or put VERCEL_TOKEN in .env.deploy.local. Then run npm run deploy again. The Codex Vercel plugin login is separate from CLI/script authentication.",
);
}
/**
* @param {ReturnType<typeof deploymentPayload>} payload
* @param {{ token: string, request?: typeof fetch, wait?: (milliseconds: number) => Promise<void>, log?: (message: string) => void }} options
*/
export async function deploy(
payload,
{
token,
request = fetch,
wait = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds)),
log = console.log,
},
) {
if (!token) throw new Error("A Vercel access token is required.");
const team = process.env.VERCEL_ORG_ID || deploymentProject.teamId;
async function api(endpoint, body) {
const response = await request(
`https://api.vercel.com${endpoint}${endpoint.includes("?") ? "&" : "?"}teamId=${encodeURIComponent(team)}`,
{
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${token}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
...(body ? { body: JSON.stringify(body) } : {}),
signal: AbortSignal.timeout(120000),
},
);
const data = await response.json();
if (!response.ok)
throw new Error(
`Vercel ${response.status}: ${data.error?.message || "Deployment request failed"}`,
);
return data;
}
let deployment = await api("/v13/deployments", payload);
if (!deployment.id) throw new Error("Vercel did not return a deployment ID.");
log(
`Uploaded ${payload.files.length} site files in one request. Waiting for Vercel…`,
);
let previousState;
for (let attempts = 0; attempts < 180; attempts++) {
const state =
deployment.readyState || deployment.status || deployment.state;
if (state !== previousState) {
log(`Build: ${state}`);
previousState = state;
}
if (state === "READY") {
const url =
payload.target === "production" &&
payload.project === deploymentProject.project
? deploymentProject.productionUrl
: `https://${deployment.url}`;
log(`Deployed: ${url}`);
return deployment;
}
if (["ERROR", "CANCELED"].includes(state))
throw new Error(
`Deployment ${state.toLowerCase()}: ${deployment.errorMessage || deployment.error?.message || "See the build log in Vercel."}`,
);
await wait(5000);
deployment = await api(
`/v13/deployments/${encodeURIComponent(deployment.id)}`,
);
}
throw new Error(
`Build is still running. Check deployment ${deployment.id} in Vercel.`,
);
}
async function main() {
const args = process.argv.slice(2);
if (args.includes("--help")) {
console.log(
"npm run deploy [-- --preview | --dry-run]\nOne upload of all site files, with no Git metadata or local secrets.\nAuthenticate once using vercel login or VERCEL_TOKEN in .env.deploy.local.",
);
return;
}
const envPath = path.join(defaultRoot, ".env.deploy.local");
if (fs.existsSync(envPath)) process.loadEnvFile(envPath);
const files = deploymentFiles();
const manifestIndex = args.indexOf("--manifest");
if (manifestIndex !== -1) {
if (!args[manifestIndex + 1])
throw new Error("--manifest requires a file path");
fs.writeFileSync(
path.resolve(args[manifestIndex + 1]),
JSON.stringify(files),
);
}
if (args.includes("--dry-run")) {
console.log(
JSON.stringify({
files: files.length,
bytes: files.reduce(
(sum, file) => sum + Buffer.from(file.data, "base64").length,
0,
),
privacyScan: "passed",
gitMetadata: false,
localSecrets: false,
target: args.includes("--preview") ? "preview" : "production",
}),
);
return;
}
await deploy(deploymentPayload(files, args.includes("--preview")), {
token: accessToken(),
});
}
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});