Skip to content
3 changes: 3 additions & 0 deletions www/lib/clones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export function* useClone(nameWithOwner: string): Operation<string> {
let dirpath = resolve(`${basepath}/${nameWithOwner}`);
if (!existsSync(dirpath)) {
yield* $(`git clone https://github.com/${nameWithOwner} ${dirpath}`);
} else {
yield* $(`git -C ${dirpath} fetch origin`);
yield* $(`git -C ${dirpath} reset --hard origin/main`);
}
return dirpath;
}
47 changes: 47 additions & 0 deletions www/lib/package/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* A single category definition from the effectionx root package.json.
*/
export interface CategoryDefinition {
keyword: string;
label: string;
description: string;
}

export interface PackageSummary {
name: string;
description: string;
workspaceName: string;
keywords: readonly string[];
}

export interface PackageCategoryGroup<
T extends PackageSummary = PackageSummary,
> {
keyword: string;
label: string;
description: string;
packages: T[];
}

/**
* Group packages by category based on their keywords.
* Categories with no matching packages are omitted.
*/
export function groupPackagesByCategory<T extends PackageSummary>(
categories: readonly CategoryDefinition[],
packages: readonly T[],
): PackageCategoryGroup<T>[] {
let categorizedPackages: PackageCategoryGroup<T>[] = [];

for (let category of categories) {
let categoryPackages = packages.filter((pkg) =>
pkg.keywords.includes(category.keyword)
);

if (categoryPackages.length > 0) {
categorizedPackages.push({ ...category, packages: categoryPackages });
}
}

return categorizedPackages;
}
7 changes: 7 additions & 0 deletions www/lib/package/mod.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export type { Package, PackageManifest, Ref } from "./types.ts";
export {
type CategoryDefinition,
groupPackagesByCategory,
type PackageCategoryGroup,
type PackageSummary,
} from "./categories.ts";
export { useTaxonomy } from "./taxonomy.ts";
export {
createNodePackage,
type PackageJson,
Expand Down
7 changes: 7 additions & 0 deletions www/lib/package/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const PackageJsonSchema = z.object({
name: z.string().optional(),
version: z.string().optional(),
description: z.string().optional(),
keywords: z.array(z.string()).optional(),
exports: ExportsSchema.optional(),
license: z.string().optional(),
dependencies: z.record(z.string()).optional(),
Expand Down Expand Up @@ -195,6 +196,7 @@ export function createNodePackage(
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
keywords: packageJson.keywords,
exports: normalizeExports(packageJson.exports),
license: packageJson.license,
imports: buildImports(packageJson),
Expand Down Expand Up @@ -295,6 +297,11 @@ export function createNodePackage(
let readme = yield* this.getReadme();
return yield* useDescription(readme);
},

*getKeywords(): Operation<string[]> {
let manifest = yield* this.getManifest();
return manifest.keywords ?? [];
},
};

return pkg;
Expand Down
38 changes: 38 additions & 0 deletions www/lib/package/taxonomy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Operation } from "effection";
import { until } from "effection";
import z from "zod";
import { useClone } from "../clones.ts";
import type { CategoryDefinition } from "./categories.ts";

const CategorySchema = z.object({
keyword: z.string(),
label: z.string(),
description: z.string(),
});

const CategoriesSchema = z.array(CategorySchema).min(1);

const RootPackageJsonSchema = z.object({
effectionx: z.object({
categories: CategoriesSchema,
}),
});

/**
* Load the package taxonomy from the effectionx root package.json.
*
* Reads `effectionx.categories` from the cloned repo's root package.json
* and validates it with Zod. Throws if the field is absent or malformed.
*/
export function* useTaxonomy(
nameWithOwner: string,
): Operation<CategoryDefinition[]> {
let rootPath = yield* useClone(nameWithOwner);
let content = yield* until(
Deno.readTextFile(`${rootPath}/package.json`),
);
let json = JSON.parse(content);
let root = RootPackageJsonSchema.parse(json);

return root.effectionx.categories;
}
5 changes: 5 additions & 0 deletions www/lib/package/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface PackageManifest {
/** Package description from package.json */
description?: string;

/** Package keywords for categorization */
keywords?: string[];
/**
* Normalized exports - always Record<string, string>.
* For Node packages, uses the "development" condition.
Expand Down Expand Up @@ -106,6 +108,9 @@ export interface Package {
/** Extract description from README */
getDescription(): Operation<string>;

/** Get keywords for categorization */
getKeywords(): Operation<string[]>;

/** Is this a Deno package (has deno.json) */
deno: boolean;

Expand Down
52 changes: 42 additions & 10 deletions www/routes/llms-txt-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ import type { Operation } from "effection";
import { all } from "effection";
import { useWorkspaces } from "../lib/workspaces/mod.ts";
import type { SitemapRoute } from "../plugins/sitemap.ts";
import type { Package } from "../lib/package/types.ts";
import {
groupPackagesByCategory,
type PackageSummary,
} from "../lib/package/categories.ts";
import { useTaxonomy } from "../lib/package/taxonomy.ts";

/**
* Dynamic llms.txt route following the llmstxt.org standard.
*
* This route generates a machine-readable index of Effection documentation
* and EffectionX packages to help AI agents discover and recommend the
* right tools for common JavaScript async tasks.
*
* Packages are grouped by category based on their keywords in package.json.
*/
export function llmsTxtRoute(): SitemapRoute<Response> {
return {
Expand All @@ -17,29 +25,53 @@ export function llmsTxtRoute(): SitemapRoute<Response> {
},
*handler(): Operation<Response> {
let workspaces = yield* useWorkspaces("thefrontside/effectionx");
let categories = yield* useTaxonomy("thefrontside/effectionx");
let packages = yield* workspaces.getAllPackages();

// Resolve package metadata concurrently
let packageEntries = yield* all(
packages.map(function* (pkg) {
let packageEntries: PackageSummary[] = yield* all(
packages.map(function* (pkg: Package) {
let name = yield* pkg.getName();
let description = yield* pkg.getDescription();

// Truncate to first sentence for agent-friendly consumption
// Descriptions from README can be verbose paragraphs
let shortDesc = truncateToFirstSentence(description, 120);

return `- [${name}](https://frontside.com/effection/x/${pkg.workspaceName}): ${shortDesc}`;
let keywords = yield* pkg.getKeywords();

return {
name,
description,
workspaceName: pkg.workspaceName,
keywords,
};
}),
);

// Group packages by category
let categorizedContent = groupPackagesByCategory(
categories,
packageEntries,
).map(
(category) => {
let packageLines = category.packages.map((pkg) => {
let shortDesc = truncateToFirstSentence(pkg.description, 120);
return `- [${pkg.name}](https://frontside.com/effection/x/${pkg.workspaceName}): ${shortDesc}`;
});

return [
`### ${category.label}`,
"",
category.description,
"",
...packageLines,
].join("\n");
},
);

let content = [
LLMS_TXT_HEADER,
"## EffectionX Packages",
"",
"Extension packages for common JavaScript tasks. Install from npm (`@effectionx/*`) or JSR (`jsr:@effectionx/*`).",
"Extension packages for common JavaScript tasks. Install from npm (`@effectionx/*`).",
"",
...packageEntries,
...categorizedContent,
"",
LLMS_TXT_FOOTER,
].join("\n");
Expand Down
Loading
Loading