Move branch-slug to @getpaseo/protocol
The app must not import @getpaseo/server. Moving the slug validation and slugify helpers into @getpaseo/protocol (already a shared dep of app, client, server) gives every package a legitimate home for the import. Drops the corresponding ./utils/branch-slug subpath from @getpaseo/server's exports map. Unblocks the Deploy App workflow, which only builds protocol/client/ audio for the app — server's dist never exists in that pipeline.
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { slugify, validateBranchSlug } from "./branch-slug.js";
|
||||
|
||||
describe("branch slug utilities", () => {
|
||||
it("normalizes display names to lowercase branch slugs", () => {
|
||||
expect(slugify("My Feature")).toBe("my-feature");
|
||||
});
|
||||
|
||||
it("collapses punctuation and whitespace to a single hyphen", () => {
|
||||
expect(slugify("My___Feature! @#$ Next")).toBe("my-feature-next");
|
||||
});
|
||||
|
||||
it("trims leading and trailing hyphens", () => {
|
||||
expect(slugify(" --- My Feature !!! ")).toBe("my-feature");
|
||||
});
|
||||
|
||||
it("enforces the 50 character slug limit", () => {
|
||||
const slug = slugify("a".repeat(60));
|
||||
|
||||
expect(slug).toBe("a".repeat(50));
|
||||
});
|
||||
|
||||
it("validates branch slugs with clear messages", () => {
|
||||
expect(validateBranchSlug("my-feature")).toEqual({ valid: true });
|
||||
expect(validateBranchSlug("")).toEqual({
|
||||
valid: false,
|
||||
error: "Branch name cannot be empty",
|
||||
});
|
||||
expect(validateBranchSlug("My Feature")).toEqual({
|
||||
valid: false,
|
||||
error:
|
||||
"Branch name must contain only lowercase letters, numbers, hyphens, and forward slashes",
|
||||
});
|
||||
expect(validateBranchSlug("-my-feature")).toEqual({
|
||||
valid: false,
|
||||
error: "Branch name cannot start or end with a hyphen",
|
||||
});
|
||||
});
|
||||
|
||||
it("is exported through the package subpath", () => {
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(join(currentDir, "..", "..", "package.json"), "utf8"),
|
||||
) as {
|
||||
exports?: Record<string, { types?: string; source?: string; default?: string }>;
|
||||
};
|
||||
|
||||
expect(packageJson.exports?.["./utils/branch-slug"]).toEqual({
|
||||
types: "./dist/server/utils/branch-slug.d.ts",
|
||||
source: "./src/utils/branch-slug.ts",
|
||||
default: "./dist/server/utils/branch-slug.js",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not import server-only modules", () => {
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(join(currentDir, "branch-slug.ts"), "utf8");
|
||||
|
||||
expect(source).not.toMatch(
|
||||
/from\s+["'](?:node:)?(?:fs|path|child_process)["']|from\s+["']node:/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Validate that a string is a valid git branch name slug.
|
||||
* Must be lowercase alphanumeric with hyphens and forward slashes only.
|
||||
*/
|
||||
export function validateBranchSlug(slug: string): {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
} {
|
||||
if (!slug || slug.length === 0) {
|
||||
return { valid: false, error: "Branch name cannot be empty" };
|
||||
}
|
||||
|
||||
if (slug.length > 100) {
|
||||
return { valid: false, error: "Branch name too long (max 100 characters)" };
|
||||
}
|
||||
|
||||
const validPattern = /^[a-z0-9-/]+$/;
|
||||
if (!validPattern.test(slug)) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"Branch name must contain only lowercase letters, numbers, hyphens, and forward slashes",
|
||||
};
|
||||
}
|
||||
|
||||
if (slug.startsWith("-") || slug.endsWith("-")) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Branch name cannot start or end with a hyphen",
|
||||
};
|
||||
}
|
||||
|
||||
if (slug.includes("--")) {
|
||||
return { valid: false, error: "Branch name cannot have consecutive hyphens" };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export const MAX_SLUG_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* Convert a string to kebab-case for branch names.
|
||||
*/
|
||||
export function slugify(input: string): string {
|
||||
const slug = input
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
|
||||
if (slug.length <= MAX_SLUG_LENGTH) {
|
||||
return slug;
|
||||
}
|
||||
|
||||
const truncated = slug.slice(0, MAX_SLUG_LENGTH);
|
||||
const lastHyphen = truncated.lastIndexOf("-");
|
||||
if (lastHyphen > MAX_SLUG_LENGTH / 2) {
|
||||
return truncated.slice(0, lastHyphen);
|
||||
}
|
||||
return truncated.replace(/-+$/, "");
|
||||
}
|
||||
@@ -30,9 +30,9 @@ import { spawnProcess } from "./spawn.js";
|
||||
import { resolvePaseoHome } from "../server/paseo-home.js";
|
||||
import { createExternalProcessEnv } from "../server/paseo-env.js";
|
||||
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
||||
import { validateBranchSlug } from "./branch-slug.js";
|
||||
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||
|
||||
export { slugify, validateBranchSlug } from "./branch-slug.js";
|
||||
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const READ_ONLY_GIT_ENV = {
|
||||
|
||||
Reference in New Issue
Block a user