From dde48e94141370c38166e20dde0cec3c347164d5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 2 Mar 2026 10:45:44 -0800 Subject: [PATCH] Move branch sanitization from contracts to new shared package Create @t3tools/shared with explicit subpath exports (no barrel) for shared utility logic. Move sanitizeBranchFragment and sanitizeFeatureBranchName to @t3tools/shared/git, keeping contracts focused on schemas. Co-Authored-By: Claude Opus 4.6 --- apps/server/package.json | 1 + .../src/git/Layers/CodexTextGeneration.ts | 2 +- apps/server/src/git/Layers/GitManager.ts | 2 +- apps/web/package.json | 1 + .../src/components/GitActionsControl.logic.ts | 10 +++--- bun.lock | 11 ++++++ packages/contracts/src/git.ts | 36 ------------------- packages/shared/package.json | 18 ++++++++++ packages/shared/src/git.ts | 33 +++++++++++++++++ packages/shared/tsconfig.json | 4 +++ 10 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 packages/shared/package.json create mode 100644 packages/shared/src/git.ts create mode 100644 packages/shared/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index 3e4d1a9fa5159b5b4600d1daa35dc003277ebc32..3eac1574b0fb71c298be6f493feb451de2b90f21 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -30,6 +30,7 @@ "@effect/language-service": "0.75.1", "@effect/vitest": "catalog:", "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "@t3tools/web": "workspace:*", "@types/bun": "^1.3.9", "@types/node": "^24.10.13", diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 4436c81def55e23e6ef8cb0d08087d82ab7351f1..9b5bbbf35387edb813776c32bcfb3c2d96501041 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto"; import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/contracts"; +import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index b7282b4055b69b02f3fdf4f96635be73b6f17897..643f276f9a2dffb23bad877983e89c51e6e8f1b8 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -1,7 +1,7 @@ import { randomUUID } from "node:crypto"; import { Effect, FileSystem, Layer, Path } from "effect"; -import { sanitizeFeatureBranchName } from "@t3tools/contracts"; +import { sanitizeFeatureBranchName } from "@t3tools/shared/git"; import { GitManagerError } from "../Errors.ts"; import { GitManager, type GitManagerShape } from "../Services/GitManager.ts"; diff --git a/apps/web/package.json b/apps/web/package.json index e1088c73eef92a8eb2eff2dc4a3de3d1c5595480..eb9960b8f993b043ab2d5cc4d2d8911cd9ad13c0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "@lexical/react": "^0.41.0", "@pierre/diffs": "^1.1.0-beta.16", "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", "@tanstack/react-router": "^1.160.2", diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index bbea434f787df5829ae6d95eb3f0f520a07335cf..53dcf982f44fbcb8856d7a0c56c41d2b38ad0823 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -1,9 +1,9 @@ -import { - type GitRunStackedActionResult, - type GitStackedAction, - type GitStatusResult, - sanitizeBranchFragment, +import type { + GitRunStackedActionResult, + GitStackedAction, + GitStatusResult, } from "@t3tools/contracts"; +import { sanitizeBranchFragment } from "@t3tools/shared/git"; export type GitActionIconName = "commit" | "push" | "pr"; diff --git a/bun.lock b/bun.lock index 1271e31d9ea5461ebb655df8d564e54656fbac06..84d11da838eddb77e765c58278f83fb141313fe9 100644 --- a/bun.lock +++ b/bun.lock @@ -50,6 +50,7 @@ "@effect/language-service": "0.75.1", "@effect/vitest": "catalog:", "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "@t3tools/web": "workspace:*", "@types/bun": "^1.3.9", "@types/node": "^24.10.13", @@ -67,6 +68,7 @@ "@lexical/react": "^0.41.0", "@pierre/diffs": "^1.1.0-beta.16", "@t3tools/contracts": "workspace:*", + "@t3tools/shared": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-query": "^5.90.0", "@tanstack/react-router": "^1.160.2", @@ -110,6 +112,13 @@ "typescript": "^5.7.3", }, }, + "packages/shared": { + "name": "@t3tools/shared", + "version": "0.0.0-alpha.1", + "devDependencies": { + "typescript": "^5.7.3", + }, + }, }, "catalog": { "@effect/language-service": "0.75.1", @@ -537,6 +546,8 @@ "@t3tools/desktop": ["@t3tools/desktop@workspace:apps/desktop"], + "@t3tools/shared": ["@t3tools/shared@workspace:packages/shared"], + "@t3tools/web": ["@t3tools/web@workspace:apps/web"], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 7e7708b74b5b4aec090042441f855c28ee2c732d..12bd739dcf955873f37173b75656c4926e73a167 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -179,39 +179,3 @@ export const GitPullResult = Schema.Struct({ upstreamBranch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), }); export type GitPullResult = typeof GitPullResult.Type; - -// Branch name utilities - -/** - * Sanitize an arbitrary string into a valid, lowercase git branch fragment. - * Strips quotes, collapses separators, limits to 64 chars. - */ -export function sanitizeBranchFragment(raw: string): string { - const normalized = raw - .trim() - .toLowerCase() - .replace(/['"`]/g, "") - .replace(/^[./\s_-]+|[./\s_-]+$/g, ""); - - const branchFragment = normalized - .replace(/[^a-z0-9/_-]+/g, "-") - .replace(/\/+/g, "/") - .replace(/-+/g, "-") - .replace(/^[./_-]+|[./_-]+$/g, "") - .slice(0, 64) - .replace(/[./_-]+$/g, ""); - - return branchFragment.length > 0 ? branchFragment : "update"; -} - -/** - * Sanitize a string into a `feature/…` branch name. - * Preserves an existing `feature/` prefix or slash-separated namespace. - */ -export function sanitizeFeatureBranchName(raw: string): string { - const sanitized = sanitizeBranchFragment(raw); - if (sanitized.includes("/")) { - return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`; - } - return `feature/${sanitized}`; -} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000000000000000000000000000000000000..222d20e9718a43bc72343fa4eeaae13abad969d1 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,18 @@ +{ + "name": "@t3tools/shared", + "version": "0.0.0-alpha.1", + "private": true, + "type": "module", + "exports": { + "./git": { + "types": "./src/git.ts", + "import": "./src/git.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/shared/src/git.ts b/packages/shared/src/git.ts new file mode 100644 index 0000000000000000000000000000000000000000..fea93d85e4d40171f1a558a3eaae22f865819d16 --- /dev/null +++ b/packages/shared/src/git.ts @@ -0,0 +1,33 @@ +/** + * Sanitize an arbitrary string into a valid, lowercase git branch fragment. + * Strips quotes, collapses separators, limits to 64 chars. + */ +export function sanitizeBranchFragment(raw: string): string { + const normalized = raw + .trim() + .toLowerCase() + .replace(/['"`]/g, "") + .replace(/^[./\s_-]+|[./\s_-]+$/g, ""); + + const branchFragment = normalized + .replace(/[^a-z0-9/_-]+/g, "-") + .replace(/\/+/g, "/") + .replace(/-+/g, "-") + .replace(/^[./_-]+|[./_-]+$/g, "") + .slice(0, 64) + .replace(/[./_-]+$/g, ""); + + return branchFragment.length > 0 ? branchFragment : "update"; +} + +/** + * Sanitize a string into a `feature/…` branch name. + * Preserves an existing `feature/` prefix or slash-separated namespace. + */ +export function sanitizeFeatureBranchName(raw: string): string { + const sanitized = sanitizeBranchFragment(raw); + if (sanitized.includes("/")) { + return sanitized.startsWith("feature/") ? sanitized : `feature/${sanitized}`; + } + return `feature/${sanitized}`; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..564a5990051fdb8cee49fbbaf99734c1a23cf60a --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} -- 2.54.0