Initial commit

This commit is contained in:
2026-07-26 12:29:43 +09:00
parent 5f290dc465
commit 50bfaeafdf
2253 changed files with 318636 additions and 196 deletions
+56
View File
@@ -0,0 +1,56 @@
import { mkdir, writeFile, cp } from "node:fs/promises";
import locales from "../locales/index.js";
import meta from "../package.json" with { type: "json" };
import { build } from "esbuild";
const customDir = process.env.ICESHRIMP_CUSTOM_DIR ?? "./custom";
Promise.all([
// copy:client:locales
(async () => {
await mkdir("./built/_client_dist_/locales", { recursive: true });
const v = { _version_: meta.version };
for (const [lang, locale] of Object.entries(locales)) {
await writeFile(
`./built/_client_dist_/locales/${lang}.${meta.version}.json`,
JSON.stringify({ ...locale, ...v }),
"utf-8",
);
}
})(),
// copy:backend:views
cp(
"./packages/backend/src/server/web/views",
"./packages/backend/built/server/web/views",
{ recursive: true },
),
// copy:backend:custom
cp(`${customDir}/assets`, "./packages/backend/assets", {
recursive: true,
}),
// build:backend:script
build({
entryPoints: [
"./packages/backend/src/server/web/boot.js",
"./packages/backend/src/server/web/bios.js",
"./packages/backend/src/server/web/cli.js",
],
outdir: "./packages/backend/built/server/web/",
define: {
LANGS: JSON.stringify(Object.keys(locales)),
},
minify: true,
}),
// build:backend:style
build({
entryPoints: [
"./packages/backend/src/server/web/style.css",
"./packages/backend/src/server/web/bios.css",
"./packages/backend/src/server/web/cli.css",
],
outdir: "./packages/backend/built/server/web/",
minify: true,
}),
]);
+55
View File
@@ -0,0 +1,55 @@
import { createReadStream, createWriteStream } from "node:fs";
import { mkdir, readdir, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import locales from "../locales/index.js";
import meta from "../package.json" with { type: "json" };
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const viewsSrc = resolve(root, "packages/backend/src/server/web/views");
const viewsOut = resolve(root, "packages/backend/built/server/web/views");
await mkdir(viewsOut, { recursive: true });
await mkdir(resolve(root, "built/_client_dist_/locales"), { recursive: true });
const localeVersion = { _version_: meta.version };
for (const [lang, locale] of Object.entries(locales)) {
await writeFile(
resolve(root, `built/_client_dist_/locales/${lang}.${meta.version}.json`),
JSON.stringify({ ...locale, ...localeVersion }),
"utf-8",
);
}
for (const entry of await readdir(viewsSrc, { withFileTypes: true })) {
if (!entry.isFile()) continue;
await pipeline(
createReadStream(resolve(viewsSrc, entry.name)),
createWriteStream(resolve(viewsOut, entry.name)),
);
}
await build({
entryPoints: [
resolve(root, "packages/backend/src/server/web/boot.js"),
resolve(root, "packages/backend/src/server/web/bios.js"),
resolve(root, "packages/backend/src/server/web/cli.js"),
],
outdir: resolve(root, "packages/backend/built/server/web"),
define: {
LANGS: JSON.stringify(Object.keys(locales)),
},
minify: true,
});
await build({
entryPoints: [
resolve(root, "packages/backend/src/server/web/style.css"),
resolve(root, "packages/backend/src/server/web/bios.css"),
resolve(root, "packages/backend/src/server/web/cli.css"),
],
outdir: resolve(root, "packages/backend/built/server/web"),
minify: true,
});
+12
View File
@@ -0,0 +1,12 @@
import { blueBright, yellowBright } from "yoctocolors"
console.log(blueBright(" ____ __ _ "));
console.log(blueBright(" / _/_______ _____/ /_ _____(_)___ ___ ____ "));
console.log(blueBright(" / // ___/ _ \\/ ___/ __ \\/ ___/ / __ `__ \\/ __ \\"));
console.log(blueBright(" _/ // /__/ __(__ ) / / / / / / / / / / / /_/ /"));
console.log(blueBright("/___/\\___/\\___/____/_/ /_/_/ /_/_/ /_/ /_/ .___/ "));
console.log(blueBright(" /_/ "));
console.log(" Currently building Iceshrimp!");
console.log(yellowBright(" Hang on for a moment, as this may take a while."));
console.log("");
+49
View File
@@ -0,0 +1,49 @@
import { rmSync } from "node:fs";
import execa from "execa";
import { join } from "node:path";
rmSync(join(import.meta.dirname, "/../packages/backend/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/backend/node_modules"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/client/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/client/node_modules"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/sw/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/sw/node_modules"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/iceshrimp-sdk/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/iceshrimp-sdk/node_modules"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../built"), { recursive: true, force: true });
rmSync(join(import.meta.dirname, "/../node_modules"), {
recursive: true,
force: true,
});
execa("yarn", ["clean"], {
cwd: join(import.meta.dirname, "/../"),
stdio: "inherit",
});
+20
View File
@@ -0,0 +1,20 @@
import { rmSync } from "node:fs";
import { join } from "node:path";
rmSync(join(import.meta.dirname, "/../packages/backend/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/client/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/sw/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../packages/iceshrimp-sdk/built"), {
recursive: true,
force: true,
});
rmSync(join(import.meta.dirname, "/../built"), { recursive: true, force: true });
+143
View File
@@ -0,0 +1,143 @@
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { basename, dirname, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
import process from "node:process";
import yaml from "js-yaml";
function loadConfig() {
const configPath = process.env.ICESHRIMP_CONFIG ?? ".config/default.yml";
const config = existsSync(configPath)
? yaml.load(readFileSync(configPath, "utf8")) ?? {}
: {};
const db = config.db ?? {};
return {
configPath,
secretsPath: process.env.ICESHRIMP_SECRETS,
url: config.url,
version: JSON.parse(readFileSync("packages/meta.json", "utf8")).version,
mediaDir: process.env.ICESHRIMP_MEDIA_DIR ?? "files",
db: {
host: process.env.PGHOST ?? db.host ?? "localhost",
port: String(process.env.PGPORT ?? db.port ?? 5432),
database: process.env.PGDATABASE ?? db.db ?? db.database ?? "iceshrimp",
user: process.env.PGUSER ?? db.user ?? "postgres",
password: process.env.PGPASSWORD ?? db.pass ?? db.password,
},
};
}
function timestamp() {
const now = new Date();
const pad = (value) => value.toString().padStart(2, "0");
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(
now.getDate(),
)}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
}
function run(command, args, env, options = {}) {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
env,
...options,
});
child.on("error", reject);
child.on("exit", (code) => {
if (code === 0) resolvePromise();
else reject(new Error(`${command} exited with code ${code}`));
});
});
}
const config = loadConfig();
const outFile = resolve(
process.argv[2] ?? `backups/iceshrimp-full-${timestamp()}.tar.gz`,
);
const workDir = mkdtempSync(resolve(tmpdir(), "iceshrimp-backup-"));
mkdirSync(dirname(outFile), { recursive: true });
try {
const env = {
...process.env,
PGHOST: config.db.host,
PGPORT: config.db.port,
PGDATABASE: config.db.database,
PGUSER: config.db.user,
};
if (config.db.password != null) env.PGPASSWORD = config.db.password;
await run(
"pg_dump",
[
"--format=custom",
"--blobs",
"--no-owner",
"--file",
resolve(workDir, "database.dump"),
config.db.database,
],
env,
);
const configDir = resolve(workDir, "config");
mkdirSync(configDir, { recursive: true });
for (const path of [config.configPath, config.secretsPath].filter(Boolean)) {
if (existsSync(path)) cpSync(path, resolve(configDir, basename(path)));
}
const mediaDir = resolve(config.mediaDir);
const outDir = dirname(outFile);
let includedMedia = false;
if (
existsSync(mediaDir) &&
outDir !== mediaDir &&
!outDir.startsWith(`${mediaDir}/`)
) {
cpSync(mediaDir, resolve(workDir, "files"), {
recursive: true,
dereference: false,
});
includedMedia = true;
}
writeFileSync(
resolve(workDir, "manifest.json"),
`${JSON.stringify(
{
type: "iceshrimp-full-backup",
version: config.version,
createdAt: new Date().toISOString(),
url: config.url,
database: config.db.database,
included: {
database: true,
config: true,
media: includedMedia,
},
restore: "yarn full:restore <this archive>",
},
null,
2,
)}\n`,
"utf8",
);
await run("tar", ["-czf", outFile, "-C", workDir, "."], process.env);
console.log(
`Full backup written to ${outFile} (${statSync(outFile).size} bytes)`,
);
} finally {
rmSync(workDir, { recursive: true, force: true });
}
+137
View File
@@ -0,0 +1,137 @@
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
} from "node:fs";
import { extname, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";
import process from "node:process";
import yaml from "js-yaml";
function loadConfig() {
const configPath = process.env.ICESHRIMP_CONFIG ?? ".config/default.yml";
const config = existsSync(configPath)
? yaml.load(readFileSync(configPath, "utf8")) ?? {}
: {};
const db = config.db ?? {};
return {
mediaDir: process.env.ICESHRIMP_MEDIA_DIR ?? "files",
db: {
host: process.env.PGHOST ?? db.host ?? "localhost",
port: String(process.env.PGPORT ?? db.port ?? 5432),
database: process.env.PGDATABASE ?? db.db ?? db.database ?? "iceshrimp",
user: process.env.PGUSER ?? db.user ?? "postgres",
password: process.env.PGPASSWORD ?? db.pass ?? db.password,
},
};
}
function run(command, args, env, options = {}) {
return new Promise((resolvePromise, reject) => {
const child = spawn(command, args, {
stdio: "inherit",
env,
...options,
});
child.on("error", reject);
child.on("exit", (code) => {
if (code === 0) resolvePromise();
else reject(new Error(`${command} exited with code ${code}`));
});
});
}
async function restoreDatabase(backupFile, db, env) {
if (extname(backupFile) === ".sql") {
await run("psql", ["--dbname", db.database, "--file", backupFile], env);
} else {
await run(
"pg_restore",
[
"--clean",
"--if-exists",
"--no-owner",
"--dbname",
db.database,
backupFile,
],
env,
);
}
}
const input = process.argv[2];
if (!input) {
console.error("Usage: yarn full:restore <backup.tar.gz|backup.dump|backup.sql>");
process.exit(1);
}
const backupFile = resolve(input);
if (!existsSync(backupFile)) {
console.error(`Backup file not found: ${backupFile}`);
process.exit(1);
}
const config = loadConfig();
const env = {
...process.env,
PGHOST: config.db.host,
PGPORT: config.db.port,
PGDATABASE: config.db.database,
PGUSER: config.db.user,
};
if (config.db.password != null) env.PGPASSWORD = config.db.password;
if (backupFile.endsWith(".tar.gz") || backupFile.endsWith(".tgz")) {
const workDir = mkdtempSync(resolve(tmpdir(), "iceshrimp-restore-"));
try {
await run("tar", ["-xzf", backupFile, "-C", workDir], process.env);
const manifestPath = resolve(workDir, "manifest.json");
if (existsSync(manifestPath)) {
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
if (manifest.type !== "iceshrimp-full-backup") {
throw new Error("Archive is not an Iceshrimp full backup");
}
}
const dumpPath = resolve(workDir, "database.dump");
if (!existsSync(dumpPath)) {
throw new Error("Archive does not contain database.dump");
}
await restoreDatabase(dumpPath, config.db, env);
const filesPath = resolve(workDir, "files");
if (existsSync(filesPath)) {
const mediaDir = resolve(config.mediaDir);
mkdirSync(mediaDir, { recursive: true });
cpSync(filesPath, mediaDir, {
recursive: true,
dereference: false,
});
console.log(`Media files restored to ${mediaDir}`);
}
const configPath = resolve(workDir, "config");
if (existsSync(configPath)) {
const restoredConfigDir = resolve(".config/restored-backup");
mkdirSync(restoredConfigDir, { recursive: true });
cpSync(configPath, restoredConfigDir, {
recursive: true,
dereference: false,
});
console.log(`Backup config files copied to ${restoredConfigDir}`);
}
} finally {
rmSync(workDir, { recursive: true, force: true });
}
} else {
await restoreDatabase(backupFile, config.db, env);
}
console.log(`Backup restored from ${backupFile}`);
+19
View File
@@ -0,0 +1,19 @@
import net from "node:net";
import readline from "node:readline";
const sock = net.connect({ path: process.argv[2] });
sock.on("data", (data) => process.stdout.write(data));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on("line", (input) => sock.write(`${input}\n`));
await new Promise((resolve, reject) => {
sock.on("close", resolve);
sock.on("error", reject);
rl.on("close", resolve);
});
sock.destroy();
rl.close();
+27
View File
@@ -0,0 +1,27 @@
import { globSync } from "glob";
import { join } from "node:path";
import { writeFileSync, readFileSync } from "node:fs";
import exec from "execa";
import { parse, stringify } from "yaml";
const files = globSync("**/package.json");
for (const file of files) {
const json = JSON.parse(readFileSync(file));
json["devDependencies"] = undefined;
writeFileSync(file, JSON.stringify(json, null, "\t"));
}
const file = join("./", ".yarnrc.yml");
const yarnrc = parse(readFileSync(file, "utf8"));
yarnrc["supportedArchitectures"] = {
cpu: ["current"],
libc: ["current"],
os: ["current"],
};
writeFileSync(file, stringify(yarnrc));
exec("yarn", ["install"], {
stdout: process.stdout,
stderr: process.stderr,
});
+14
View File
@@ -0,0 +1,14 @@
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { join } from "node:path";
const execPromise = promisify(exec);
console.log("compressing client");
await execPromise("brotli -f *.js *.json *.css", {
cwd: join(import.meta.dirname, "../built/_client_dist_"),
});
console.log("compressing service worker");
await execPromise("brotli -f sw.js", {
cwd: join(import.meta.dirname, "../built/_sw_dist_"),
});
+14
View File
@@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "node:fs";
import exec from "execa";
const json = JSON.parse(readFileSync("package.json"));
const match = json['version'].match(/^[\d.]*(?:-pre\d+|)?/);
const version = match ? `${match[0]}-dev` : "dev";
const revision = process.argv.length > 2
? process.argv[2]
: (await exec("git", ["rev-parse", "--short", "HEAD"])).stdout;
json['version'] = `${version}-${revision}`;
console.log(`Package version was updated to ${json['version']}`);
writeFileSync("package.json", JSON.stringify(json, null, '\t'));