Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
265e014
feat: add typed HashQL TypeScript SDK
indietyp Oct 12, 2026
62ffc8c
feat: split HashQL authoring API
indietyp Oct 14, 2026
ff4df26
feat: add HashQL draft program API
indietyp Oct 14, 2026
2e31783
feat: add structural HashQL type shorthand
indietyp Oct 14, 2026
6a5f8e3
fix: preserve module exports and optional types
indietyp Oct 14, 2026
563f557
feat: add synchronous draft authoring API
indietyp Oct 14, 2026
8741bc6
feat: add Draft filter examples and input algebra tests
indietyp Oct 14, 2026
8628a46
feat: add explicit predicates and lexical imports
indietyp Oct 15, 2026
14af22e
feat: wrap Draft callback exceptions in results
indietyp Oct 15, 2026
2a6097c
fix: capture exceptions in HashQL callbacks
indietyp Oct 15, 2026
24b0970
refactor: improve HashQL schema and filter tests
indietyp Oct 15, 2026
d0fd364
docs: expand hashql sdk api documentation
indietyp Oct 15, 2026
921eda2
docs: add HashQL TypeScript API reference
indietyp Oct 16, 2026
293b958
feat: add Effect API analysis CLI to HashQL docs
indietyp Oct 16, 2026
89fb06a
chore: configure API extractor warning levels
indietyp Oct 16, 2026
5b0a1fe
feat: generate reference pages from API analysis
indietyp Oct 16, 2026
ec6b027
chore: clean up typescript docs linting
indietyp Oct 16, 2026
3640f69
refactor: adapt typescript docs to effect 4.0.0
indietyp Oct 16, 2026
a897a45
feat: build HashQL TypeScript docs site
indietyp Oct 16, 2026
bf34383
feat: add namespace index cards
indietyp Oct 16, 2026
691d3a0
feat: document TypeScript package entry points
indietyp Oct 16, 2026
45b6f98
refactor: remove Do authoring API and docs
indietyp Oct 19, 2026
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
4 changes: 4 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.generated/
.source/
.twoslash/
content/docs/reference/
606 changes: 606 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/LICENSE.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFromSource } from "fumadocs-core/search/server";

import { source } from "../../../lib/source";

export const { GET } = createFromSource(source, { language: "english" });
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
} from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import { notFound } from "next/navigation";

import { getMDXComponents } from "../../../components/mdx";
import { source } from "../../../lib/source";

import type { Metadata } from "next";
import type { ReactNode } from "react";

interface PageProps {
params: Promise<{ slug?: string[] }>;
}

export default async function Page({ params }: PageProps): Promise<ReactNode> {
const { slug } = await params;
const page = source.getPage(slug);
if (page === undefined) {
notFound();
}
const Content = page.data.body;
const components = getMDXComponents({
// oxlint-disable-next-line id-length -- MDX uses HTML tag names as component keys.
a: createRelativeLink(source, page),
});
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
{/* Qualified type names may be longer than the mobile viewport. */}
{/* oxlint-disable-next-line react/forbid-component-props -- DocsBody exposes className for content layout. */}
<DocsBody className="[&_h2]:wrap-anywhere">
<Content components={components} />
</DocsBody>
</DocsPage>
);
}

export const generateStaticParams = (): { slug: string[] }[] =>
source.generateParams();

export const generateMetadata = async ({
params,
}: PageProps): Promise<Metadata> => {
const { slug } = await params;
const page = source.getPage(slug);
if (page === undefined) {
notFound();
}
return { title: page.data.title, description: page.data.description };
};
19 changes: 19 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";

import { source } from "../../lib/source";

import type { ReactNode } from "react";

const navigation = { title: "HashQL TypeScript", url: "/docs" };

export default function Layout({
children,
}: {
children: ReactNode;
}): ReactNode {
return (
<DocsLayout tree={source.getPageTree()} nav={navigation}>
{children}
</DocsLayout>
);
}
15 changes: 15 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/app/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "fumadocs-twoslash/twoslash.css";

/* A name that links to its documentation is underlined, whether or not Twoslash gives it a hover. */
.twoslash a[href] {
text-decoration-line: underline;
text-decoration-color: color-mix(in oklab, currentColor 35%, transparent);
text-underline-offset: 0.25em;
}

.twoslash a[href]:hover {
text-decoration-color: currentColor;
}
21 changes: 21 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RootProvider } from "fumadocs-ui/provider/next";

import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./global.css";

export const metadata: Metadata = { title: "HashQL TypeScript" };

export default function Layout({
children,
}: {
children: ReactNode;
}): ReactNode {
return (
<html lang="en" suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";

export default function Home(): never {
redirect("/docs");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Card } from "fumadocs-ui/components/card";

import type { ReactNode } from "react";

/** A link to a page or a folder, showing its name and size above the summary its module comment opens with. */
export const IndexEntry = ({
name,
count,
href,
children,
}: {
name: string;
count: string;
href: string;
children?: ReactNode;
}): ReactNode => (
<Card
href={href}
title={
<span className="flex items-baseline justify-between gap-3">
<span className="font-mono">{name}</span>
<span className="text-fd-muted-foreground shrink-0 font-mono text-xs font-normal">
{count}
</span>
</span>
}
>
{children}
</Card>
);
18 changes: 18 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/components/mdx.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Popup, PopupContent } from "fumadocs-twoslash/ui";
import defaultComponents from "fumadocs-ui/mdx";

import { IndexEntry } from "./index-entry";
import { ReferenceTrigger } from "./reference-trigger";

import type { MDXComponents } from "mdx/types";

export const getMDXComponents = (
components?: MDXComponents,
): MDXComponents => ({
...defaultComponents,
IndexEntry,
Popup,
PopupContent,
PopupTrigger: ReferenceTrigger,
...components,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";

import { PopupTrigger } from "fumadocs-twoslash/ui";

import type { ComponentProps, ReactNode } from "react";

export const ReferenceTrigger = ({
href,
...props
}: ComponentProps<typeof PopupTrigger> & { href?: string }): ReactNode => {
if (href === undefined) {
return <PopupTrigger {...props} />;
}
return (
<PopupTrigger
{...props}
nativeButton={false}
render={
<a href={href} role="link">
{props.children}
</a>
}
/>
);
};
10 changes: 10 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: HashQL TypeScript
description: Build typed HashQL expressions and programs in TypeScript.
---

## API reference

The [Expression module](./reference/program/Expression) describes symbolic values, their semantic types and input requirements.

The reference currently covers Expression. Examples construct and print J-Expr; they do not execute graph queries.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"title": "HashQL TypeScript",
"pages": ["index", "reference"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"package": "@local/hashql-typescript-docs",
"dependencies": [
"@local/hashql",
"@local/oxlint",
"@local/tsconfig"
],
"tasks": {
"build": {
"dependsOn": [
"@local/hashql#build",
"@local/oxlint#build",
"codegen"
],
"env": [
"NEXT_TELEMETRY_DISABLED"
]
},
"codegen": {
"dependsOn": [
"doc:transform"
]
},
"dev": {
"dependsOn": [
"@local/hashql#build",
"@local/oxlint#build",
"codegen"
],
"cache": false,
"persistent": true,
"env": [
"NEXT_TELEMETRY_DISABLED",
"PORT"
]
},
"doc:analyze": {
"dependsOn": [
"doc:declarations"
]
},
"doc:declarations": {
"dependsOn": []
},
"doc:transform": {
"dependsOn": [
"doc:analyze"
]
},
"lint:oxlint": {
"dependsOn": [
"@local/hashql#build",
"@local/oxlint#build",
"codegen"
]
},
"lint:tsc": {
"dependsOn": [
"@local/hashql#build",
"@local/oxlint#build",
"codegen"
]
},
"start": {
"dependsOn": [
"build"
],
"cache": false,
"persistent": true,
"env": [
"NEXT_TELEMETRY_DISABLED",
"PORT"
]
},
"test:unit": {
"dependsOn": [],
"env": [
"TEST_COVERAGE"
]
}
}
}
88 changes: 88 additions & 0 deletions libs/@local/hashql/sdk/typescript-docs/lib/reference-links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Array, Effect, Option, Struct, flow, pipe } from "effect";
import { splitTokens } from "shiki/core";

import * as CodeBlock from "../src/CodeBlock.js";

import type { Element, ElementContent } from "hast";
import type { ShikiTransformer } from "shiki";

const isElement = (node: ElementContent): node is Element =>
node.type === "element";

// A popup's content is the hover text, which holds no names of the code.
const triggerIn = (node: Element): Option.Option<Element> =>
pipe(
node.children,
Array.filter(isElement),
Array.filter(({ tagName }) => tagName !== "PopupContent"),
Array.findFirst((child) =>
pipe(
child,
Option.liftPredicate(({ tagName }) => tagName === "PopupTrigger"),
Option.orElse(() => triggerIn(child)),
),
),
);

// Twoslash has replaced a hovered name's text with its popup by now, so its trigger becomes the link and the hover stays.
const link = (node: Element): void => {
for (const child of Array.filter(node.children, isElement)) {
const url = child.properties["data-reference"];

if (typeof url === "string") {
delete child.properties["data-reference"];
Option.match(triggerIn(child), {
onNone: () => {
child.tagName = "a";
child.properties.href = url;
},
onSome: (trigger) => {
trigger.properties.href = url;
},
});
} else {
link(child);
}
}
};

/** Links the names a signature block's meta lists to the pages that document them. Place it after the Twoslash transformer. */
export const referenceLinks = (): ShikiTransformer => ({
name: "hashql:reference-links",
tokens(lines) {
const code = pipe(
lines,
Array.map(flow(Array.map(Struct.get("content")), Array.join(""))),
Array.join("\n"),
);
// A transformer runs synchronously inside Shiki, and a block whose links do not fit its code fails the build.
const links = Effect.runSync(
CodeBlock.place(code, this.options.meta?.__raw ?? ""),
);
const split = splitTokens(
lines,
Array.flatMap(links, ({ start, end }) => [start, end]),
);

// Shiki copies a token's attributes onto the element it renders.
for (const token of Array.flatten(split)) {
const found = Array.findFirst(
links,
({ start, end }) =>
start <= token.offset && token.offset + token.content.length <= end,
);

if (Option.isSome(found)) {
token.htmlAttrs = {
...token.htmlAttrs,
"data-reference": found.value.url,
};
}
}

return split;
},
code(code) {
link(code);
},
});
Loading
Loading