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
93 changes: 85 additions & 8 deletions scripts/build-llms.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { readFile, writeFile } from "node:fs/promises";
import { dirname, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

import { API_SECTIONS } from "./llms-small/api.mjs";

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const OUTPUT_DIR = resolve(ROOT, process.argv[2] ?? "dist");
const SITE_ORIGIN = new URL(
Expand All @@ -11,6 +13,8 @@ const SITE_ORIGIN = new URL(
);
const INDEX_PATH = resolve(OUTPUT_DIR, "llms.txt");
const FULL_PATH = resolve(OUTPUT_DIR, "llms-full.txt");
const SMALL_PATH = resolve(OUTPUT_DIR, "llms-small.txt");
const PRIMER_PATH = resolve(ROOT, "scripts/llms-small/primer.md");

const DOCUMENT_LINK = /^- \[([^\]]+)\]\(([^)]+\.md)\)(?:: .*)?$/gm;

Expand Down Expand Up @@ -74,13 +78,21 @@ function curateIndex(index) {
}

const fullCorpusUrl = new URL("/llms-full.txt", SITE_ORIGIN).toString();
if (!lines.some((line) => line.includes(fullCorpusUrl))) {
lines.splice(
firstSectionIndex,
0,
const smallCorpusUrl = new URL("/llms-small.txt", SITE_ORIGIN).toString();
const pointers = [
[
fullCorpusUrl,
`For a single-file corpus, use [llms-full.txt](${fullCorpusUrl}).`,
""
);
],
[
smallCorpusUrl,
`For a short primer with one line per solid-js export, use [llms-small.txt](${smallCorpusUrl}).`,
],
]
.filter(([url]) => !lines.some((line) => line.includes(url)))
.map(([, pointer]) => pointer);
if (pointers.length > 0) {
lines.splice(firstSectionIndex, 0, ...pointers, "");
}

return `${absolutizeMarkdownLinks(lines.join("\n"))}\n`;
Expand Down Expand Up @@ -114,6 +126,69 @@ async function buildFullCorpus(title, documents) {
].join("\n");
}

// The primer source starts with an HTML comment naming where it was copied
// from; the comment is for editors of this repository, not for the file.
function stripLeadingComment(markdown) {
return markdown.replace(/^\s*<!--[\s\S]*?-->\s*/, "");
}

// Prettier pads Markdown table cells to the widest row; the padding is a
// third of the primer's tables, and llms-small.txt is read by tools, not
// people, so the cells shrink to one space on each side.
function compactTables(markdown) {
return markdown
.split("\n")
.map((line) =>
line.startsWith("|")
? line.replace(/-{3,}/g, "---").replace(/ {2,}/g, " ")
: line
)
.join("\n");
}

function renderApiTable() {
return API_SECTIONS.map((section) =>
[
`### ${section.title}`,
"",
"| Export | Purpose |",
"| --- | --- |",
...section.exports.map(
([name, purpose]) => `| \`${name}\` | ${purpose} |`
),
].join("\n")
).join("\n\n");
}

async function buildSmallCorpus(title) {
const primer = stripLeadingComment(await readFile(PRIMER_PATH, "utf8"))
.trim()
.split("\n");
if (!primer[0]?.startsWith("# ")) {
throw new Error("The primer must start with a level-one heading");
}
const primerTitle = primer[0].slice(2).trim();
const primerBody = compactTables(primer.slice(1).join("\n").trim());

return [
`# ${title}: ${primerTitle}`,
"",
"> The short form for tools with a small context budget: the rules that differ from React and Solid 1.x, the 1.x to 2.0 corrections, and one line per `solid-js` export.",
"",
`Index: ${new URL("/llms.txt", SITE_ORIGIN)}`,
`Full corpus: ${new URL("/llms-full.txt", SITE_ORIGIN)}`,
"",
primerBody,
"",
"## solid-js exports",
"",
"One line per public export of `solid-js`; the reference pages in the index carry the signatures.",
"",
renderApiTable(),
"",
].join("\n");
}

const generatedIndex = await readFile(INDEX_PATH, "utf8");
const documents = parseDocuments(generatedIndex);
if (documents.length === 0) {
Expand All @@ -125,16 +200,18 @@ if (!title) {
throw new Error("llms.txt does not contain a title");
}

const [curatedIndex, fullCorpus] = await Promise.all([
const [curatedIndex, fullCorpus, smallCorpus] = await Promise.all([
Promise.resolve(curateIndex(generatedIndex)),
buildFullCorpus(title, documents),
buildSmallCorpus(title),
]);

await Promise.all([
writeFile(INDEX_PATH, curatedIndex, "utf8"),
writeFile(FULL_PATH, fullCorpus, "utf8"),
writeFile(SMALL_PATH, smallCorpus, "utf8"),
]);

console.log(
`Generated llms.txt and llms-full.txt from ${documents.length} documents.`
`Generated llms.txt and llms-full.txt from ${documents.length} documents, and llms-small.txt from the primer and ${API_SECTIONS.reduce((n, section) => n + section.exports.length, 0)} exports.`
);
257 changes: 257 additions & 0 deletions scripts/llms-small/api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// One line per public runtime export of `solid-js`, for llms-small.txt.
// Names follow packages/solid/src/index.ts in the solidjs/solid repository;
// purposes follow packages/solid/CHEATSHEET.md and the reference pages under
// src/routes/reference/(1)solid-js. Exports marked `@internal` upstream or
// hidden by scripts/extract-solid-ref.mjs (compiler and renderer ABI such as
// `createComponent`, `createOwner`, `enableHydration`, `$PROXY`) stay out.
// When an export is added or removed upstream, change this list and the
// reference pages together.

export const API_SECTIONS = [
{
title: "Signals and memos",
exports: [
[
"createSignal",
"`[get, set]` reactive value; `createSignal(fn)` is a writable derived value",
],
[
"createMemo",
"read-only derived value; a returned Promise is pending until it lands",
],
[
"createDeferred",
"async memo that keeps its last value while a refetch is in flight",
],
["untrack", "run `fn` without subscribing to the reads inside it"],
["flush", "apply queued writes now instead of on the next microtask"],
["isEqual", "default equality of signals and memos"],
],
},
{
title: "Effects and lifecycle",
exports: [
[
"createEffect",
"`(compute, apply)`: compute tracks, apply runs effects and may return a cleanup",
],
[
"onSettled",
"run once after the current activity settles; return a cleanup (replaces `onMount`)",
],
[
"onCleanup",
"cleanup tied to a reactive run or owner disposal; for primitive authors",
],
[
"createRenderEffect",
"effect that runs during render, for DOM bindings; app code uses `createEffect`",
],
[
"createTrackedEffect",
"single-callback effect that tracks and applies in one scope; rare",
],
[
"createReaction",
"one-shot tracked callback: `track(() => dep())` runs `fn` on the next change",
],
],
},
{
title: "Stores",
exports: [
[
"createStore",
"deep reactive object; `set(draft => { ... })` writes through the draft",
],
[
"createProjection",
"read-only derived store; `{ key }` gives keyed reconciliation",
],
[
"reconcile",
"`reconcile(data, key)(draft.part)` merges new data into a store sub-tree by key",
],
[
"deep",
"track every nested level of a store (default tracking is per property)",
],
[
"snapshot",
"plain untracked copy of a store for JSON, storage and tests (replaces `unwrap`)",
],
["isWrappable", "whether a value would become a store proxy"],
[
"merge",
"merge props-like objects into one reactive proxy (replaces `mergeProps`)",
],
[
"omit",
"reactive proxy of props without the listed keys (replaces `splitProps`)",
],
],
},
{
title: "Async, actions and transitions",
exports: [
[
"action",
"generator mutation: optimistic writes, `yield` async work, `refresh`; reverts on failure",
],
[
"createOptimistic",
"signal whose writes inside an action show now and revert when it settles",
],
["createOptimisticStore", "store form of `createOptimistic`"],
[
"isPending",
"true while a change to the values read inside `fn` is in flight",
],
[
"latest",
"read the in-flight value during a transition instead of the committed one",
],
[
"refresh",
"re-run one source after a server write; quiet unless `affects` was declared",
],
[
"affects",
"mark a source or store slot as pending while in-flight work will change it",
],
[
"until",
"Promise for the first time a reactive predicate turns truthy; `yield` it in an action",
],
[
"resolve",
"Promise for the first settled value of a reactive expression (tests)",
],
[
"NotReadyError",
"thrown by a read of an async source before its first value; boundaries handle it",
],
["TimeoutError", "rejection from `until` when its `timeout` passes"],
[
"enableExternalSource",
"adapter so Solid tracks reads of another reactive system (MobX, Vue)",
],
],
},
{
title: "Components and context",
exports: [
["children", "resolve `props.children` once; `.toArray()` helper"],
[
"flatten",
"resolve nested accessors and arrays in a children tree (`children` memoizes it)",
],
[
"createContext",
"context for a subtree; `<Ctx value={v}>` is the provider; no default means no provider throws",
],
["useContext", "read the nearest context value"],
[
"lazy",
"code-split component; pending through `<Loading>` while its chunk loads",
],
[
"lazyModule",
"cached dynamic import of a non-component module; `.preload()` and `.peek()`",
],
[
"dynamicComponent",
"component that re-resolves `fn` to a component; the factory behind `dynamic`",
],
[
"createUniqueId",
"stable id that matches between server and client (`for`, `aria-labelledby`)",
],
],
},
{
title: "Flow components",
exports: [
[
"For",
"keyed list; callback gets `(item, i())`, or `(item(), i)` with `keyed={false}`",
],
["Repeat", "render `count` times; `i` is a plain number"],
[
"Show",
"render children when `when` is truthy; a function child gets a narrowed accessor",
],
["Switch", "render the first `<Match>` whose `when` is truthy"],
["Match", "one branch of `<Switch>`"],
[
"Loading",
"async boundary (replaces `Suspense`); `on={deps}` re-arms it",
],
[
"Errored",
"error boundary (replaces `ErrorBoundary`); `fallback={(err, reset) => ...}`",
],
[
"Reveal",
"reveal order of sibling `<Loading>` boundaries (replaces `SuspenseList`)",
],
[
"mapArray",
"reactive array map behind `<For>`; reuses entries for unchanged items",
],
["repeat", "reactive count-based render behind `<Repeat>`"],
],
},
{
title: "Owners and roots",
exports: [
[
"createRoot",
"owned reactive scope with `dispose()`; tests and library entry points",
],
[
"getOwner",
"current owner, to capture across an async gap in library code",
],
[
"runWithOwner",
"run `fn` under an owner; `runWithOwner(null, fn)` detaches a root",
],
["getObserver", "the currently tracking computation, or `null`"],
["isDisposed", "whether an owner has been disposed, to ignore late work"],
],
},
{
title: "Hydration (advanced)",
exports: [
["NoHydration", "opt a subtree out of client hydration"],
["Hydration", "opt a subtree back in under `<NoHydration>`"],
["isHydrating", "whether code runs while claiming server-rendered DOM"],
[
"isHydratable",
"whether the calling owner sits where hydration applies",
],
[
"onHydrationEnd",
"run once after the root pass and every streamed boundary have resumed",
],
],
},
{
title: "Dev and observability",
exports: [
[
"DEV",
"dev tier: devtools hooks, graph traversal, diagnostics reporting; dev builds only",
],
[
"OBSERVE",
"observe tier: records channel, diagnostics channel, attribution slot",
],
[
"configureClientErrors",
"hook called once per error when a boundary renders its fallback",
],
],
},
];
Loading
Loading