Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 22 additions & 36 deletions toolkit-docs-generator/scripts/sync-toolkit-sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { TOOLKITS as DESIGN_SYSTEM_TOOLKITS } from "@arcadeai/design-system/metadata/toolkits";
import { getToolkitSlug } from "../../app/_lib/toolkit-slug";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -83,21 +84,13 @@ const CATEGORY_ORDER = [
"customer-support",
"others",
];
const CATEGORY_SET = new Set(CATEGORY_ORDER);

const CAPITAL_LETTER_REGEX = /([A-Z])/g;
const FIRST_CHARACTER_REGEX = /^./;
const CAMEL_BOUNDARY = /([a-z0-9])([A-Z])/g;

const IDENTIFIER_KEY_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;

/**
* Convert a CamelCase string to kebab-case.
* Must stay in sync with toKebabCase in app/_lib/toolkit-slug.ts.
*/
function toKebabCase(value: string): string {
return value.replace(CAMEL_BOUNDARY, "$1-$2").toLowerCase();
}

type ToolkitJson = {
id?: string;
label?: string;
Expand All @@ -114,10 +107,12 @@ function renderObjectKey(key: string): string {
if (IDENTIFIER_KEY_REGEX.test(key)) {
return key;
}
const escaped = key.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `"${escaped}"`;
return JSON.stringify(key);
}

const normalizeCategory = (category: string | null | undefined): string =>
category && CATEGORY_SET.has(category) ? category : "others";

export type ToolkitInfo = {
id: string;
slug: string;
Expand Down Expand Up @@ -231,21 +226,6 @@ function readToolkitJson(dataDir: string, slug: string): ToolkitJson | null {
return null;
}

function getDocsSlugFromLink(docsLink?: string | null): string | null {
if (!docsLink) {
return null;
}

try {
const url = new URL(docsLink);
const segments = url.pathname.split("/").filter(Boolean);
return segments.at(-1) ?? null;
} catch {
const segments = docsLink.split("/").filter(Boolean);
return segments.at(-1) ?? null;
}
}

/**
* Read toolkit JSON and extract label if available
*/
Expand Down Expand Up @@ -294,8 +274,13 @@ function resolveToolkitInfo(
): ToolkitInfoEntry | null {
const jsonData = readToolkitJson(dataDir, slug);
const toolkitId = jsonData?.id ?? slug;
const docsSlug =
getDocsSlugFromLink(jsonData?.metadata?.docsLink) ?? toKebabCase(toolkitId);
const docsSlug = getToolkitSlug({
id: toolkitId,
docsLink: jsonData?.metadata?.docsLink,
});
if (!docsSlug) {
return null;
}
const designSystemToolkit = TOOLKITS.find(
(t) => t.id.toLowerCase() === toolkitId.toLowerCase()
);
Expand All @@ -306,8 +291,9 @@ function resolveToolkitInfo(

// Keep sidebar routes aligned with static params: toolkit JSON is source of
// truth for category, with design system as fallback when JSON is missing.
const category =
jsonData?.metadata?.category ?? designSystemToolkit?.category ?? "others";
const category = normalizeCategory(
jsonData?.metadata?.category ?? designSystemToolkit?.category
);
const labelFromDesignSystem = designSystemToolkit?.label ?? null;
const labelFromJson = jsonData?.label ?? jsonData?.name ?? null;
const typeFromJson = jsonData?.metadata?.type ?? null;
Expand Down Expand Up @@ -382,6 +368,7 @@ export function generateCategoryMeta(
category: string,
integrationsBasePath: string
): string {
const safeCategory = normalizeCategory(category);
const byLabel = (a: ToolkitInfo, b: ToolkitInfo) =>
a.label.localeCompare(b.label);

Expand All @@ -393,19 +380,18 @@ export function generateCategoryMeta(
.sort(byLabel);

const renderEntry = (t: ToolkitInfo) => {
// Escape any quotes in the label
const escapedLabel = t.label.replace(/"/g, '\\"');
const href = `${integrationsBasePath}/${safeCategory}/${t.slug}`;
return ` ${renderObjectKey(t.slug)}: {
title: "${escapedLabel}",
href: "${integrationsBasePath}/${category}/${t.slug}",
title: ${JSON.stringify(t.label)},
href: ${JSON.stringify(href)},
}`;
};

const renderSeparator = (title: string) => {
const key = `-- ${title}`;
return ` ${renderObjectKey(key)}: {
type: "separator",
title: "${title}",
title: ${JSON.stringify(title)},
}`;
};

Expand Down Expand Up @@ -460,7 +446,7 @@ export function generateMainMeta(activeCategories: string[]): string {
.map((cat) => {
const displayName = CATEGORY_NAMES[cat] || cat;
return ` ${renderObjectKey(cat)}: {
title: "${displayName}",
title: ${JSON.stringify(displayName)},
}`;
})
.join(",\n");
Expand Down
61 changes: 61 additions & 0 deletions toolkit-docs-generator/tests/scripts/sync-toolkit-sidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,51 @@ describe("buildToolkitInfoList", () => {
expect(entry?.category).toBe("sales");
});

it("maps unknown categories to the safe others directory", () => {
createToolkitJson("unsafe", {
id: "UnsafeToolkit",
label: "Unsafe",
metadata: {
category: "../../outside",
docsLink: "https://docs.arcade.dev/integrations/unsafe",
},
});

const [entry] = buildToolkitInfoList(TEST_DATA_DIR);

expect(entry?.category).toBe("others");
});

it("falls back when docsLink does not end in a safe slug", () => {
createToolkitJson("unsafe", {
id: "UnsafeToolkit",
label: "Unsafe",
metadata: {
category: "development",
docsLink: "https://docs.arcade.dev/bad%2Fslug",
},
});

const [entry] = buildToolkitInfoList(TEST_DATA_DIR);

expect(entry?.slug).toBe("unsafe-toolkit");
});

it("uses the shared normalized fallback for legacy toolkit IDs", () => {
createToolkitJson("unsafe", {
id: "Unsafe Toolkit",
label: "Unsafe",
metadata: {
category: "development",
docsLink: "https://docs.arcade.dev/bad%2Fslug",
},
});

const [entry] = buildToolkitInfoList(TEST_DATA_DIR);

expect(entry?.slug).toBe("unsafetoolkit");
});

it("should dedupe entries that share a docsLink slug", () => {
createToolkitJson("upclickapi", {
id: "UpclickApi",
Expand Down Expand Up @@ -505,6 +550,22 @@ describe("generateCategoryMeta", () => {
expect(result).toContain('title: "Test \\"Quoted\\" Label"');
});

it("should escape backslashes and newlines in labels", () => {
const toolkits: ToolkitInfo[] = [
{
id: "test",
slug: "test",
label: "Test\\Label\nNext",
category: "others",
navGroup: "optimized",
},
];

const result = generateCategoryMeta(toolkits, "others", "/preview");

expect(result).toContain('title: "Test\\\\Label\\nNext"');
});

it("should handle empty array", () => {
const result = generateCategoryMeta([], "productivity", "/preview");

Expand Down
Loading