diff --git a/apps/api/package.json b/apps/api/package.json index 69f8986d9..44b9b5112 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "node --enable-source-maps .", "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/apps/discord-bot/package.json b/apps/discord-bot/package.json index 9dc7db193..cf20f84d3 100644 --- a/apps/discord-bot/package.json +++ b/apps/discord-bot/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "node --enable-source-maps .", "build": "swc src --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/apps/scripts/package.json b/apps/scripts/package.json index 8315ef054..c3558c389 100644 --- a/apps/scripts/package.json +++ b/apps/scripts/package.json @@ -5,11 +5,13 @@ "type": "module", "scripts": { "lint": "oxlint", + "typecheck": "tsc --noEmit", "lint:ci": "oxlint --format=github", "api-key": "node src/api-key.js", "delete-sessions": "node src/delete-sessions.js", "limit-redis": "node src/limit-redis.js", "purge": "node src/purge.js", + "setup-worktree": "node src/setup-worktree.ts", "rank-emojis": "node src/rank-emojis.js", "timestamp": "node --no-warnings src/timestamp.js", "validate-commands": "node --no-warnings src/validate-commands.js" diff --git a/apps/scripts/src/clean-dist.ts b/apps/scripts/src/clean-dist.ts new file mode 100644 index 000000000..3feca7d4a --- /dev/null +++ b/apps/scripts/src/clean-dist.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +/** + * Removes files from `dist` whose source file in `src` no longer exists. + * + * Runs in a package directory as its turbo `clean` task, which `build` depends on, so it + * runs before both a real build and a cache restore. A cache restore writes outputs on + * top of `dist` without deleting anything, so this is what keeps deleted or renamed + * sources from leaving orphaned files behind. + * + * Only orphans are removed, so `dist` is never empty while `pnpm build:watch` rebuilds + * and running apps keep their files. + * + * Usage: node ../../apps/scripts/src/clean-dist.ts + */ + +import { existsSync, readdirSync, rmSync, rmdirSync } from "node:fs"; +import { join } from "node:path"; + +const DIST = "dist"; +const SRC = "src"; + +// The source extensions swc compiles to each output extension, with the same base name. +// .cjs isn't compiled, so --copy-files copies it as is +const SOURCE_EXTENSIONS: Record = { + ".js": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".es6", ".es"], + ".mjs": [".mts"], + ".cjs": [".cts", ".cjs"], +}; + +/** + * @param file path relative to dist + */ +function hasSource(file: string): boolean { + const match = /^(.+?)(\.[cm]?js)(\.map)?$/.exec(file); + + // Files that swc copies as is with --copy-files + if (!match) return existsSync(join(SRC, file)); + + const [, base, outputExtension] = match; + + // swc doesn't compile declaration files, so a .d.js output is always an orphan + if (base.endsWith(".d")) return false; + + return SOURCE_EXTENSIONS[outputExtension].some((extension) => + existsSync(join(SRC, `${base}${extension}`)), + ); +} + +/** + * @param dir path relative to dist + * @returns the number of orphaned files removed + */ +function prune(dir: string): number { + let removed = 0; + + for (const entry of readdirSync(join(DIST, dir), { withFileTypes: true })) { + const path = join(dir, entry.name); + + if (entry.isDirectory()) { + removed += prune(path); + } else if (!hasSource(path)) { + rmSync(join(DIST, path), { force: true }); + removed++; + } + } + + if (dir && readdirSync(join(DIST, dir)).length === 0) + rmdirSync(join(DIST, dir)); + + return removed; +} + +if (existsSync(DIST)) { + const removed = prune(""); + if (removed > 0) console.log(`Removed ${removed} orphaned files from dist`); +} diff --git a/apps/scripts/src/setup-worktree.ts b/apps/scripts/src/setup-worktree.ts new file mode 100644 index 000000000..7c9798b07 --- /dev/null +++ b/apps/scripts/src/setup-worktree.ts @@ -0,0 +1,563 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +/** + * Prepares a checkout (the main clone or a linked git worktree) for development. + * Run it in each new worktree, e.g. as a T3 Code script that runs on worktree creation. + * + * Only uses node builtins because it runs before `pnpm install`. Node runs it as + * TypeScript by stripping the types, so it only uses erasable syntax. + * + * Safe to run in many new worktrees at once: cache entries are written to a + * temporary folder and renamed into place, restores are checked against the + * backgrounds in git, and fetches that lose a lock race are retried. + * + * Usage: node apps/scripts/src/setup-worktree.ts [--skip-build] + */ + +import { + constants, + cpSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + rmdirSync, + statSync, + symlinkSync, +} from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; +import { execFileSync } from "node:child_process"; + +// git prints forward slashes on Windows, resolve normalizes them to match COMMON_DIR +const ROOT = resolve( + git( + ["rev-parse", "--show-toplevel"], + resolve(import.meta.dirname, "../../.."), + ), +); +const COMMON_DIR = resolve(ROOT, git(["rev-parse", "--git-common-dir"], ROOT)); +// The first entry is the main checkout. Unlike dirname(COMMON_DIR), this also holds +// when its git dir isn't
/.git (e.g. cloned with --separate-git-dir) +const MAIN_ROOT = resolve( + git(["worktree", "list", "--porcelain"], ROOT) + .split("\n")[0] + .slice("worktree ".length), +); +const IS_LINKED_WORKTREE = MAIN_ROOT !== ROOT; +const BLUR_CACHE_DIR = join( + COMMON_DIR, + "statsify-cache", + "blurred-backgrounds", +); + +const STALE_TEMP_MS = 60 * 60 * 1000; + +// Setup runs unattended (T3 makes the agent wait for it), so a git credential prompt +// would hang forever. Make git (and Git Credential Manager's GUI) fail instead; saved +// credentials still work +const FETCH_ENV = { GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" }; + +const TEXTURE_PACK = "assets/public/minecraft-textures/default"; +const PUBLIC_ASSETS = "assets/public"; +// Packages blur.mjs uses to decode, blur, and encode the backgrounds +const BLUR_DEPENDENCIES = ["skia-canvas", "stackblur-canvas"]; +const PRIVATE_ASSETS = "assets/private"; + +function git(args: string[], cwd = ROOT): string { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); +} + +function run( + command: string, + args: string[], + cwd = ROOT, + env: Record = {}, +) { + execFileSync(command, args, { + cwd, + env: { ...process.env, ...env }, + stdio: "inherit", + // pnpm is a .cmd shim on Windows, which needs a shell. Other commands (git) must not + // go through one, since cmd.exe would split path arguments that contain spaces + shell: process.platform === "win32" && command === "pnpm", + }); +} + +function log(title: string, message: string) { + console.log(`\n! ${title} ${message}`); +} + +/** + * Symlinks a file or directory from the main checkout, copying it when symlinks are not permitted. + * @param path path relative to the repo root + */ +function linkFromMain(path: string): boolean { + const source = join(MAIN_ROOT, path); + const target = join(ROOT, path); + + if (existsSync(target) || !existsSync(source)) return false; + + mkdirSync(dirname(target), { recursive: true }); + + try { + // Windows junctions only work for directories, and need no extra permissions. + // A file symlink needs Developer Mode or admin rights, so it falls back to a copy + symlinkSync( + source, + target, + statSync(source).isDirectory() ? "junction" : "file", + ); + } catch { + cpSync(source, target, { recursive: true }); + } + + return true; +} + +/** + * Only config.js is linked. The loader tries config.json first, but loads it with a + * plain import(), which node rejects for JSON files. + */ +function linkConfig() { + if (!IS_LINKED_WORKTREE) return; + + if (linkFromMain("config.js")) + log("Config", `linked config.js from ${MAIN_ROOT}`); + else if (!existsSync(join(ROOT, "config.js"))) + log("Config", "missing, copy config.schema.js to config.js and fill it in"); +} + +/** + * Agent instruction files aren't committed, so a new worktree only gets them when the + * main checkout has them locally + */ +function linkAgentInstructions() { + if (!IS_LINKED_WORKTREE) return; + + for (const file of ["AGENTS.md", "CLAUDE.md"]) { + if (linkFromMain(file)) log("Agents", `linked ${file} from ${MAIN_ROOT}`); + } +} + +interface Submodule { + /** git stores the submodule's repo under .git/modules/ */ + name: string; + path: string; +} + +function submodules(): Submodule[] { + let output: string; + + try { + output = git([ + "config", + "--file", + ".gitmodules", + "--null", + "--get-regexp", + String.raw`^submodule\..*\.path$`, + ]); + } catch { + return []; + } + + // With --null, entries look like `submodule..path\n\0`, so names (which + // can contain dots) and paths can contain spaces + return output + .split("\0") + .filter(Boolean) + .map((entry) => { + const newline = entry.indexOf("\n"); + const key = entry.slice(0, newline); + const path = entry.slice(newline + 1); + return { name: key.slice("submodule.".length, -".path".length), path }; + }); +} + +/** + * Checks that a submodule repo has a commit, fetching it when it is missing. + * @param repo git arguments that select the submodule repo + * @returns whether the commit is available + */ +function hasCommit(repo: string[], sha: string): boolean { + const exists = () => { + try { + git([...repo, "cat-file", "-e", `${sha}^{commit}`]); + return true; + } catch { + return false; + } + }; + + if (exists()) return true; + + // Concurrent setups fetching the same submodule repo can fail to lock refs, + // so check whether the other fetch brought the commit in and retry once + for (let attempt = 0; attempt < 2; attempt++) { + try { + run("git", [...repo, "fetch", "origin"], ROOT, FETCH_ENV); + } catch { + // Checked below + } + + if (exists()) return true; + } + + // A commit no remote branch reaches anymore (e.g. from a deleted, squash-merged + // branch) only arrives when fetched directly, like `git submodule update` does + try { + run("git", [...repo, "fetch", "origin", sha], ROOT, FETCH_ENV); + } catch { + // Checked below + } + + return exists(); +} + +/** + * Moves an existing submodule checkout to the commit this branch pins, unless it is + * on a branch or has local changes. + */ +function updateSubmodule(path: string, sha: string) { + const target = join(ROOT, path); + const head = git(["rev-parse", "HEAD"], target); + if (head === sha) return; + + let onBranch = true; + + try { + git(["symbolic-ref", "-q", "HEAD"], target); + } catch { + onBranch = false; + } + + if (onBranch || git(["status", "--porcelain"], target) !== "") { + log( + "Submodule", + `${path} is at ${head.slice(0, 8)} but this branch pins ${sha.slice(0, 8)}, leaving it because it is on a branch or has local changes`, + ); + return; + } + + if (!hasCommit(["-C", target], sha)) { + log("Submodule", `could not fetch ${path} at ${sha.slice(0, 8)}`); + return; + } + + git(["checkout", "--detach", sha], target); + log("Submodule", `updated ${path} to ${sha.slice(0, 8)}`); +} + +function setupSubmodule({ name, path }: Submodule) { + const target = join(ROOT, path); + // Read the pinned commit from the index like `git submodule update` does, so a + // staged but uncommitted submodule bump isn't undone. Lines look like + // `160000 0\t` + const sha = git(["ls-files", "--stage", "--", path]).split(/\s+/)[1]; + if (!sha) return; + + if (existsSync(join(target, ".git"))) { + try { + updateSubmodule(path, sha); + } catch { + log( + "Submodule", + `could not update ${path}, continuing with it as is. If it is broken in a worktree, delete ${path} and rerun setup`, + ); + } + + return; + } + + if (!IS_LINKED_WORKTREE) { + try { + run("git", ["submodule", "update", "--init", path], ROOT, FETCH_ENV); + } catch { + log("Submodule", `could not clone ${path}, continuing without it`); + } + + return; + } + + // Cloning here would put a full copy in this worktree's own git dir, so every + // worktree would clone it again, and a private repo can hang on a credential prompt + const moduleDir = join(COMMON_DIR, "modules", name); + + if (!existsSync(moduleDir)) { + log( + "Submodule", + `${path} isn't set up in the main checkout, continuing without it. Run \`git submodule update --init ${path}\` there to share it with worktrees`, + ); + return; + } + + if (!hasCommit(["--git-dir", moduleDir], sha)) { + log("Submodule", `could not fetch ${path}, continuing without it`); + return; + } + + // Worktrees that were deleted without `git worktree remove` leave stale entries + // behind. git locks worktrees while it adds them, so this can't remove one that a + // concurrent setup is still creating + try { + git(["--git-dir", moduleDir, "worktree", "prune"]); + } catch { + // Only housekeeping + } + + if (existsSync(target) && readdirSync(target).length === 0) rmdirSync(target); + + try { + git(["--git-dir", moduleDir, "worktree", "add", "--detach", target, sha]); + } catch { + log( + "Submodule", + `could not check out ${path}, continuing without it. If ${path} has leftover files, delete it and rerun setup`, + ); + return; + } + + log("Submodule", `checked out ${path} at ${sha.slice(0, 8)}`); +} + +function linkTexturePack() { + // Linking into a public assets folder that isn't checked out would create it and + // block checking out the submodule there on the next run + const publicAssets = join(ROOT, PUBLIC_ASSETS, ".git"); + + if ( + IS_LINKED_WORKTREE && + existsSync(publicAssets) && + linkFromMain(TEXTURE_PACK) + ) + log("Textures", `linked ${TEXTURE_PACK}`); + + if (!existsSync(join(ROOT, TEXTURE_PACK))) + log("Textures", `missing, add a 1.8.9 texture pack to ${TEXTURE_PACK}`); +} + +/** + * @param backgrounds file names of the tracked backgrounds + * @returns whether `output` has a blurred file for every background + */ +function isComplete(output: string, backgrounds: string[]): boolean { + if (!existsSync(output)) return false; + + const blurred = new Set(readdirSync(output)); + return backgrounds.every((background) => blurred.has(background)); +} + +/** + * Copies a cache entry to `output` and checks that every background made it, since + * another setup can evict the entry while it is being copied. + * @param backgrounds file names of the tracked backgrounds + * @returns whether the restore is complete + */ +function restoreBlurCache( + entry: string, + output: string, + backgrounds: string[], +): boolean { + try { + rmSync(output, { recursive: true, force: true }); + // Copy on write where the filesystem supports it, so restoring costs no extra disk space + cpSync(entry, output, { + recursive: true, + mode: constants.COPYFILE_FICLONE, + }); + } catch { + return false; + } + + return isComplete(output, backgrounds); +} + +/** + * Stores `output` as the cache entry for `key` and evicts the entries that were there + * before it. The entry is copied to a temporary folder and renamed into place, so + * concurrent setups never see a partial entry. + * + * Only entries listed before the rename are evicted. Two setups can't evict each other, + * since each would have to list the cache after the other's rename, so the most recently + * stored entry always survives. A setup that finds its key already stored evicts nothing. + * Temporary folders older than an hour are left over from crashed setups and evicted too. + * @returns whether this call stored the entry + */ +function storeBlurCache( + cacheDir: string, + key: string, + output: string, +): boolean { + const entry = join(cacheDir, key); + const temp = `${entry}.tmp-${process.pid}`; + let previous: string[]; + + // The cache is optional, so failing to write it (a full disk, an unwritable .git) + // must not stop setup + try { + mkdirSync(cacheDir, { recursive: true }); + cpSync(output, temp, { recursive: true, mode: constants.COPYFILE_FICLONE }); + previous = readdirSync(cacheDir); + } catch { + log("Backgrounds", "could not write the blur cache, continuing without it"); + rmSync(temp, { recursive: true, force: true }); + return false; + } + + try { + renameSync(temp, entry); + } catch { + // Another setup already stored this key + rmSync(temp, { recursive: true, force: true }); + return false; + } + + for (const name of previous) { + if (name === key) continue; + + const path = join(cacheDir, name); + + try { + // A fresh temporary folder belongs to a setup that is still copying. Copying + // keeps updating its mtime, so an old one was left behind by a crashed setup + if ( + name.includes(".tmp-") && + Date.now() - statSync(path).mtimeMs < STALE_TEMP_MS + ) + continue; + + rmSync(path, { recursive: true, force: true }); + } catch { + // Another setup is evicting it too, or it was this setup's temporary folder + } + } + + return true; +} + +/** + * @returns `@` of a package installed in `dir`, or undefined when it + * isn't installed + */ +function installedVersion(dir: string, name: string): string | undefined { + try { + const manifest = JSON.parse( + readFileSync(join(dir, "node_modules", name, "package.json"), "utf8"), + ) as { version: string }; + + return `${name}@${manifest.version}`; + } catch { + return undefined; + } +} + +/** + * The blur output only depends on the backgrounds, the blur script, and the installed + * versions of the packages that decode, blur, and encode the images. It is cached by + * the git object ids of the first two and those versions. Uncommitted changes to the + * backgrounds or blur script always regenerate. + */ +function blurPrivateBackgrounds() { + const assets = join(ROOT, PRIVATE_ASSETS); + if (!existsSync(join(assets, "package.json"))) return; + + const output = join(assets, "out", "backgrounds"); + const dirty = + git(["status", "--porcelain", "--", "backgrounds", "blur.mjs"], assets) !== + ""; + + const versions = BLUR_DEPENDENCIES.map((name) => + installedVersion(assets, name), + ); + + const key = + dirty || versions.includes(undefined) + ? undefined + : [ + git(["rev-parse", "HEAD:backgrounds"], assets), + git(["rev-parse", "HEAD:blur.mjs"], assets), + ...versions, + ].join("-"); + + const cached = key && join(BLUR_CACHE_DIR, key); + + // blur.mjs writes one output per background, with the same name. Only tracked + // backgrounds are part of the key, so ignored files like .DS_Store don't count. + // -z keeps git from quoting names with special characters, like café.png + const backgrounds = key + ? git(["ls-tree", "-z", "--name-only", "HEAD:backgrounds"], assets) + .split("\0") + .filter(Boolean) + : []; + + if (cached && existsSync(cached)) { + if (restoreBlurCache(cached, output, backgrounds)) { + log("Backgrounds", `restored from cache ${relative(MAIN_ROOT, cached)}`); + return; + } + + // Either evicted while it was being copied, stored incomplete, or the copy itself + // failed (e.g. a full disk). Only delete an entry that is itself incomplete, or an + // incomplete entry could never be replaced, but a local copy failure must not evict + // a good entry other setups are restoring from + if (isComplete(cached, backgrounds)) { + log("Backgrounds", "could not restore from cache"); + } else { + log("Backgrounds", "cache entry is incomplete, replacing it"); + rmSync(cached, { recursive: true, force: true }); + } + } + + log("Backgrounds", "blurring private backgrounds, this takes a while"); + run("pnpm", ["blur"], assets); + + if (!key) return; + + if (!isComplete(output, backgrounds)) { + log("Backgrounds", "blur output is incomplete, not caching it"); + return; + } + + if (storeBlurCache(BLUR_CACHE_DIR, key, output)) + log("Backgrounds", "cached for future checkouts"); +} + +log( + "Setup", + `${ROOT}${IS_LINKED_WORKTREE ? ` (worktree of ${MAIN_ROOT})` : ""}`, +); + +linkConfig(); +linkAgentInstructions(); +for (const submodule of submodules()) setupSubmodule(submodule); +linkTexturePack(); + +// Also blurs the public backgrounds, through their postinstall +run("pnpm", ["install", "--frozen-lockfile"]); + +// The backgrounds are only needed when an app renders, so a failed blur must not +// stop the build that every package import depends on +try { + blurPrivateBackgrounds(); +} catch { + log( + "Backgrounds", + "could not blur the private backgrounds, continuing without them. Rerun setup to retry", + ); +} + +if (!process.argv.includes("--skip-build")) run("pnpm", ["build"]); + +log("Setup", "done"); diff --git a/apps/scripts/src/validate-commands.js b/apps/scripts/src/validate-commands.js index 19d7c097c..2e431e9a9 100644 --- a/apps/scripts/src/validate-commands.js +++ b/apps/scripts/src/validate-commands.js @@ -6,18 +6,18 @@ * https://github.com/Statsify/statsify/blob/main/LICENSE */ -import commands from "discord-bot/commands.json" assert { type: "json" }; -import z from "zod"; +import commands from "discord-bot/commands.json" with { type: "json" }; +import { z } from "zod"; function toEnum(...args) { return z.union(args.map((arg) => z.literal(arg))); } const name = z.string().min(3).max(32); -const name_localizations = z.optional(z.record(name)); +const name_localizations = z.optional(z.record(z.string(), name)); const description = z.string().min(1).max(100); -const description_localizations = z.optional(z.record(description)); +const description_localizations = z.optional(z.record(z.string(), description)); const choicesSchema = z.object({ // this is intentional since choice names can go up to 100 characters @@ -52,27 +52,27 @@ const commandSchema = z.object({ type: z.optional(toEnum(1, 2, 3)), }); +let hasInvalidCommand = false; + for (const [commandName, command] of Object.entries(commands.commands)) { - try { - commandSchema.parse(command); - } catch (e) { - console.error(e); - console.log(command); - - for (const error of e.errors) { - console.error(error.message); - console.error(`${command.name}.${error.path.join(".")}`); - } - - console.error(`Command "${commandName}" is invalid.`); - process.exit(1); + const result = commandSchema.safeParse(command); + if (result.success) continue; + + for (const issue of result.error.issues) { + console.error(issue.message); + console.error(`${command.name}.${issue.path.join(".")}`); } + + console.error(`Command "${commandName}" is invalid.`); + hasInvalidCommand = true; } +if (hasInvalidCommand) process.exit(1); + const commandChars = {}; -const findLongestLocalizationLength = (localizations) => - Math.max(...Object.values(localizations).map((l) => l.length)); +const findLongestLocalizationLength = (localizations = {}) => + Math.max(0, ...Object.values(localizations).map((l) => l.length)); const findOptionsLength = (options = []) => options @@ -81,7 +81,7 @@ const findOptionsLength = (options = []) => option.name.length + findLongestLocalizationLength(option.description_localizations) + ( - option?.choices?.map((choice) => choice.value.length + choice.name.length) ?? [] + option?.choices?.map((choice) => `${choice.value}`.length + choice.name.length) ?? [] ).reduce((a, b) => a + b, 0) ) .reduce((a, b) => a + b, 0); diff --git a/apps/scripts/tsconfig.json b/apps/scripts/tsconfig.json index dd3fcc5f4..9cf09065a 100644 --- a/apps/scripts/tsconfig.json +++ b/apps/scripts/tsconfig.json @@ -1,4 +1,11 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"] -} \ No newline at end of file + "compilerOptions": { + // node runs the .ts scripts by stripping types, which only works for erasable syntax + "erasableSyntaxOnly": true, + "types": ["node"] + }, + // The .js scripts aren't type checked (checkJs is off), and including them pulls in + // the workspace packages they import, which use non-erasable syntax like enums + "include": ["src/**/*.ts"] +} diff --git a/apps/support-bot/package.json b/apps/support-bot/package.json index 19b914dd0..310ffe2d5 100644 --- a/apps/support-bot/package.json +++ b/apps/support-bot/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "node --enable-source-maps .", "build": "swc src --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/apps/verify-server/package.json b/apps/verify-server/package.json index bfb0482a0..c0bf93f70 100644 --- a/apps/verify-server/package.json +++ b/apps/verify-server/package.json @@ -7,6 +7,7 @@ "scripts": { "start": "node --enable-source-maps .", "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 28880a59f..1ad319c96 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/assets/package.json b/packages/assets/package.json index 93f7611d5..89ade1479 100644 --- a/packages/assets/package.json +++ b/packages/assets/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/discord/package.json b/packages/discord/package.json index e7a829451..6bf63d8e0 100644 --- a/packages/discord/package.json +++ b/packages/discord/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/logger/package.json b/packages/logger/package.json index e5e46c44a..48767a5a2 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/math/package.json b/packages/math/package.json index b33551203..31e39edb1 100644 --- a/packages/math/package.json +++ b/packages/math/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/rendering/package.json b/packages/rendering/package.json index 5abf42564..d3df8cf3a 100644 --- a/packages/rendering/package.json +++ b/packages/rendering/package.json @@ -15,6 +15,7 @@ }, "scripts": { "build": "swc src --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/schemas/package.json b/packages/schemas/package.json index 0aed6a9f8..1a7de4608 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths --copy-files", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github" diff --git a/packages/skin-renderer/turbo.json b/packages/skin-renderer/turbo.json new file mode 100644 index 000000000..20c00abe4 --- /dev/null +++ b/packages/skin-renderer/turbo.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/Cargo.toml", + "$TURBO_ROOT$/Cargo.lock" + ], + "outputs": ["pkg/**", "*.node"] + } + } +} diff --git a/packages/util/package.json b/packages/util/package.json index 9c3cda222..e9e1983fc 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -6,6 +6,7 @@ "types": "src/index.ts", "scripts": { "build": "swc src --config-file ../../.swcrc --out-dir dist --strip-leading-paths", + "clean": "node ../../apps/scripts/src/clean-dist.ts", "typecheck": "tsc --noEmit", "lint": "oxlint", "lint:ci": "oxlint --format=github", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 795b137c5..519ea70d0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - apps/** - packages/** + - "!packages/skin-renderer/pkg" - assets/** onlyBuiltDependencies: diff --git a/turbo.json b/turbo.json index cf52f50df..f280231e0 100644 --- a/turbo.json +++ b/turbo.json @@ -1,35 +1,52 @@ { "$schema": "https://turborepo.org/schema.json", - "globalDependencies": ["pnpm.lock", "tsconfig.json", ".swcrc"], + "globalDependencies": ["pnpm-lock.yaml"], "ui": "tui", "tasks": { "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.swcrc"], "env": ["API_URL", "API_KEY"], "outputs": ["dist/**", ".next/**", "pkg/**"], - "dependsOn": ["^build"] + "dependsOn": ["clean", "^build"] + }, + "clean": { + "cache": false }, "typecheck": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/tsconfig.base.json"], "outputs": [], "dependsOn": ["^typecheck"] }, "test": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.swcrc"], "outputs": [], "dependsOn": ["build", "^test"] }, "test:coverage": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.swcrc"], "outputs": ["coverage"], "dependsOn": ["build", "^test:coverage"] }, "lint": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/.oxlintrc.jsonc", + "$TURBO_ROOT$/tsconfig.base.json" + ], "outputs": [], "dependsOn": [] }, "lint:ci": { + "inputs": [ + "$TURBO_DEFAULT$", + "$TURBO_ROOT$/.oxlintrc.jsonc", + "$TURBO_ROOT$/tsconfig.base.json" + ], "outputs": [], "dependsOn": [] }, "fmt": { - "outputs": [], + "cache": false, "dependsOn": [] } }