diff --git a/.changeset/quiet-machines-document.md b/.changeset/quiet-machines-document.md new file mode 100644 index 0000000..505743f --- /dev/null +++ b/.changeset/quiet-machines-document.md @@ -0,0 +1,7 @@ +--- +"@typeonce/effect-machine": patch +--- + +Document the nested configuration and callback APIs used by `Machine.states`, event protocols, `Machine.make`, `Definition.handle`, execution, transitions, and state invocation. + +The documentation site now presents those core authoring APIs on a standalone guide-reference page while keeping module pages focused on public exports. Parameters remain grouped under their point of use with searchable signatures, lifecycle contexts, target semantics, defaults, source links, focused examples, and a complete nested page outline. Redundant configuration headings are omitted, and each parameter is presented as a named API block with its signature, description, and attached source link. Long signatures stay contained within their documentation blocks across responsive layouts. diff --git a/api-reference.config.json b/api-reference.config.json index eab21e6..12ca97d 100644 --- a/api-reference.config.json +++ b/api-reference.config.json @@ -25,6 +25,146 @@ "export": "./Machine", "source": "src/Machine.ts", "barrel": ".", + "referenceSections": [ + { + "title": "Define state topology", + "description": "Declare the complete state tree and its schema-backed values before constructing the machine.", + "entries": [ + { "declaration": "states" }, + { "declaration": "state" } + ] + }, + { + "title": "Define event protocols", + "description": "Describe public input, machine-local events, outward notifications, and parent ownership.", + "entries": [ + { "declaration": "events" }, + { "declaration": "internalEvents" }, + { "declaration": "emittedEvents" }, + { "declaration": "parent" }, + { "declaration": "optionalParent" } + ] + }, + { + "title": "Create the definition", + "description": "Combine topology and protocols into a reusable machine definition with an explicit initial target.", + "entries": [ + { "declaration": "make" } + ] + }, + { + "title": "Implement state behavior", + "description": "Attach state-local actions, transitions, output, and invoked lifecycles to the definition.", + "entries": [ + { + "reflection": "Definition.handle", + "label": "handle", + "kind": "method", + "owner": "Definition", + "ownerKind": "interface", + "usageSections": [ + "State handler configuration", + "Transition declarations", + "Handler and resolver contexts", + "State invocation" + ] + } + ] + }, + { + "title": "Run and observe", + "description": "Start or resume an executable machine and subscribe to its state over time.", + "entries": [ + { "declaration": "start" }, + { "declaration": "resume" }, + { "declaration": "watch" } + ] + } + ], + "usageSections": [ + { + "owner": "states", + "title": "State node configuration", + "description": "Properties accepted while declaring the state tree passed to `Machine.states`.", + "roots": [ + { "reflection": "Machine.AtomicStateNodeConfig", "label": "Atomic and final states" }, + { "reflection": "Machine.CompoundStateNodeConfig", "label": "Compound states" }, + { "reflection": "Machine.ParallelStateNodeConfig", "label": "Parallel states" }, + { "reflection": "Machine.HistoryStateNodeConfig", "label": "History states" }, + { "reflection": "Machine.ChoiceStateNodeConfig", "label": "Choice states" }, + { "reflection": "Machine.StateNodeAnnotations", "label": "Annotations" } + ] + }, + { + "owner": "make", + "title": "Machine definition configuration", + "description": "Parameters accepted by `Machine.make` and its initial-state declaration.", + "roots": [ + { "declaration": "make", "parameter": "config", "label": "Configuration" }, + { "reflection": "Machine.InitialTransitionTarget", "label": "Initial target resolution" } + ] + }, + { + "owner": "Definition", + "ownerKind": "interface", + "title": "State handler configuration", + "description": "Properties accepted for each state inside `Definition.handle`.", + "roots": [ + { "reflection": "Machine.ActiveStateConfig", "label": "Active states" }, + { "reflection": "Machine.FinalStateConfig", "label": "Final states" }, + { "reflection": "Machine.ChoiceStateConfig", "label": "Choice states", "members": ["choice"] }, + { "reflection": "Machine.HandlerNode", "label": "Nested handler tree", "members": ["states", "history"] }, + { "reflection": "Machine.OutputHandlerConfig", "label": "Output functions", "members": ["output"] }, + { "reflection": "Machine.HistoryDefaultEntry", "label": "History defaults" } + ] + }, + { + "owner": "Definition", + "ownerKind": "interface", + "title": "Transition declarations", + "description": "Target selection, resolution, branching, and commands available to state and invocation transitions.", + "roots": [ + { "reflection": "Machine.TargetSelector", "label": "Target selection" }, + { "reflection": "Machine.TargetBuilder", "label": "Resolver target construction" }, + { "reflection": "Machine.TransitionSelector", "label": "Named branch selection", "nested": true }, + { "reflection": "Machine.TransitionTarget", "label": "Selected target" }, + { "reflection": "Machine.TransitionBranchInput", "label": "Named branches" }, + { "reflection": "Machine.DeclineCapability", "label": "Declinable transitions" }, + { "reflection": "Enqueue", "label": "Enqueued commands" } + ] + }, + { + "owner": "Definition", + "ownerKind": "interface", + "title": "Handler and resolver contexts", + "description": "Values supplied to state actions, transitions, completion handlers, choices, and output functions.", + "roots": [ + { "reflection": "MachineReferences", "label": "Machine references" }, + { "reflection": "Machine.HandlerContext", "label": "Event handlers" }, + { "reflection": "Machine.StateActionContext", "label": "Entry and exit actions" }, + { "reflection": "Machine.AlwaysContext", "label": "Eventless transitions" }, + { "reflection": "Machine.DoneContext", "label": "State completion" }, + { "reflection": "Machine.ChoiceContext", "label": "Choice resolution" }, + { "reflection": "Machine.FinalOutputContext", "label": "Final output" }, + { "reflection": "Machine.ParallelOutputContext", "label": "Parallel output" } + ] + }, + { + "owner": "Definition", + "ownerKind": "interface", + "title": "State invocation", + "description": "State-owned Effect, Stream, timer, logic, and child lifecycles declared through `invoke`.", + "roots": [ + { "reflection": "Machine.InvokeSelector", "label": "Invocation sources" }, + { "reflection": "Machine.InvokeBuilder", "label": "Lifecycle handlers" }, + { "reflection": "Machine.InvokeContext", "label": "Source context" }, + { "reflection": "Machine.InvokeDoneContext", "label": "Completion context" }, + { "reflection": "Machine.InvokeFailureContext", "label": "Failure context" }, + { "reflection": "Machine.InvokeElementContext", "label": "Stream element context" }, + { "reflection": "Machine.InvokeSnapshotContext", "label": "Child snapshot context" } + ] + } + ], "examples": [ "decodeSnapshot", "encodeSnapshot", diff --git a/scripts/api-reference-site/api-reference-site.test.mjs b/scripts/api-reference-site/api-reference-site.test.mjs index 172b79d..0c775f8 100644 --- a/scripts/api-reference-site/api-reference-site.test.mjs +++ b/scripts/api-reference-site/api-reference-site.test.mjs @@ -10,6 +10,7 @@ import { parseChangelog, parseChangeset, renderChangelogPage, + renderGuidePage, renderIndexPage, renderLayout, renderMarkdown, @@ -194,6 +195,156 @@ test("renders collapsible category navigation with declaration anchors", () => { assert.match(html, /

make<\/h3>/) }) +test("renders declaration-owned usage sections and documented members in the guide", () => { + const declaration = { + name: "make", + kind: "variable", + description: "Creates a machine.", + examples: [], + see: [], + usageSections: [{ + owner: "make", + title: "Machine configuration", + description: "Parameters accepted by make.", + roots: [{ + label: "Configuration", + description: "Complete machine configuration.", + examples: [], + members: [{ + name: "id", + signature: "readonly id?: string", + description: "Stable definition identifier.", + sourceUrl: "https://github.com/example/repo/blob/main/src/Machine.ts#L1", + examples: [], + parameters: [], + members: [] + }] + }] + }] + } + const module = { + api: { + declarationCount: 1, + description: "State machine APIs", + referenceSections: [{ + title: "Create the definition", + description: "Construct the machine.", + entries: [{ origin: { type: "declaration", name: "make", kind: "variable" }, api: declaration }] + }], + groups: [{ category: "constructors", declarations: [declaration] }] + }, + export: "./Machine", + label: "Machine", + route: "Machine" + } + const html = renderGuidePage({ ...site, modules: [module], guideModule: module }) + assert.doesNotMatch(html, />Machine configuration<\//) + assert.match(html, /href="#make-usage-machine-configuration-configuration">Configuration<\/a>/) + assert.match(html, /

Configuration<\/h4>/) + assert.match(html, /id="make-usage-machine-configuration-configuration-id" aria-labelledby="make-usage-machine-configuration-configuration-id-title"/) + assert.match(html, /

id<\/h5>/) + assert.match(html, /readonly<\/span> id/) + assert.match(html, /string<\/span>/) + assert.match(html, /Stable definition identifier\./) + assert.doesNotMatch(html, />Usage referenceSource { + const states = { + name: "states", + kind: "variable", + description: "Defines state topology.", + examples: [], + see: [], + usageSections: [] + } + const make = { + name: "make", + kind: "variable", + description: "Creates a definition.", + examples: [], + see: [], + usageSections: [] + } + const handlerUsage = { + owner: "Definition", + ownerKind: "interface", + title: "State handler configuration", + description: "State-local behavior.", + roots: [] + } + const definition = { + name: "Definition", + kind: "interface", + description: "Reusable definition model.", + examples: [], + see: [], + usageSections: [handlerUsage] + } + const helper = { + name: "helper", + kind: "function", + description: "Supporting helper.", + examples: [], + see: [], + usageSections: [] + } + const handle = { + name: "handle", + kind: "method", + description: "Adds typed state handlers.", + examples: [], + see: [], + usageSections: [handlerUsage] + } + const module = { + api: { + declarationCount: 4, + description: "State machine APIs", + referenceSections: [{ + title: "Define state topology", + description: "Start with the states.", + entries: [{ origin: { type: "declaration", name: "states", kind: "variable" }, api: states }] + }, { + title: "Create the definition", + description: "Construct and implement the machine.", + entries: [ + { origin: { type: "declaration", name: "make", kind: "variable" }, api: make }, + { origin: { type: "reflection", reflection: "Definition.handle" }, api: handle } + ] + }], + groups: [ + { category: "constructors", declarations: [helper, make, states] }, + { category: "models", declarations: [definition] } + ] + }, + export: "./Machine", + label: "Machine", + route: "Machine" + } + + const guideSite = { ...site, modules: [module], guideModule: module } + const guideHtml = renderGuidePage(guideSite) + assert.ok(guideHtml.indexOf("Define state topology") < guideHtml.indexOf('id="states"')) + assert.ok(guideHtml.indexOf('id="states"') < guideHtml.indexOf("Create the definition")) + assert.match(guideHtml, /

handle<\/h3>/) + assert.doesNotMatch(guideHtml, /id="helper"/) + assert.match(guideHtml, /class="navigation-guide is-current"/) + assert.ok(guideHtml.indexOf("navigation-changelog") < guideHtml.indexOf("navigation-guide is-current")) + + const moduleHtml = renderModulePage(guideSite, module) + assert.match(moduleHtml, /

helper<\/h3>/) + assert.match(moduleHtml, /

make<\/h3>/) + assert.match(moduleHtml, /

states<\/h3>/) + assert.match(moduleHtml, /

Definition<\/h3>/) + assert.doesNotMatch(moduleHtml, /Define state topology/) + assert.doesNotMatch(moduleHtml, /State handler configuration/) + assert.doesNotMatch(moduleHtml, /id="handle"/) +}) + test("renders canonical and social metadata without exposing the internal channel", () => { const html = renderLayout(site, { content: '
API reference
', @@ -264,11 +415,13 @@ test("keeps the internal Effect channel out of the homepage label", () => { test("generates manifest, robots, and sitemap URLs from the deployment base", () => { const manifest = siteManifest(site) + const siteWithGuide = { ...site, guideModule: site.modules[0] } assert.equal(manifest.start_url, "/docs/") assert.equal(manifest.icons[2].purpose, "maskable") assert.match(renderRobots(site), /Sitemap: https:\/\/docs\.example\.com\/docs\/sitemap\.xml/) assert.match(renderSitemap(site), /https:\/\/docs\.example\.com\/docs\/Machine\/<\/loc>/) assert.match(renderSitemap(site), /https:\/\/docs\.example\.com\/docs\/changelog\/<\/loc>/) + assert.match(renderSitemap(siteWithGuide), /https:\/\/docs\.example\.com\/docs\/guide\/<\/loc>/) }) test("accepts only pathless HTTPS production origins", () => { diff --git a/scripts/api-reference-site/assets/styles.css b/scripts/api-reference-site/assets/styles.css index 63f0103..1b4cb5f 100644 --- a/scripts/api-reference-site/assets/styles.css +++ b/scripts/api-reference-site/assets/styles.css @@ -82,6 +82,8 @@ } html { + max-width: 100%; + overflow-x: clip; scroll-behavior: smooth; scroll-padding-top: 6rem; } @@ -90,6 +92,8 @@ body { background: var(--background); color: var(--text); margin: 0; + max-width: 100%; + overflow-x: clip; } a { @@ -261,7 +265,9 @@ kbd { grid-template-columns: var(--width-navigation) minmax(0, 1fr) var(--width-toc); margin: 0 auto; max-width: 100rem; + min-width: 0; min-height: calc(100vh - 4rem); + width: 100%; } .module-navigation { @@ -312,7 +318,8 @@ kbd { padding: 0.55rem 0.65rem; } -.module-navigation .navigation-changelog { +.module-navigation .navigation-changelog, +.module-navigation .navigation-guide { margin-top: 0.15rem; } @@ -352,9 +359,11 @@ kbd { .reference-hero, .module-reference, +.guide-reference, .changelog { margin: 0 auto; max-width: 64rem; + min-width: 0; } .reference-hero { @@ -387,6 +396,7 @@ h3 { .reference-hero h1, .module-header h1, +.guide-header h1, .changelog-header h1 { font-size: clamp(2.35rem, 4.5vw, 4.2rem); line-height: 0.95; @@ -395,6 +405,7 @@ h3 { .reference-hero > p, .module-header > p, +.guide-header > p, .changelog-header > p { color: var(--text-soft); font-size: 1.06rem; @@ -563,6 +574,49 @@ h3 { font-size: 0.62rem; } +.guide-toc-section + .guide-toc-section { + margin-top: 1.1rem; +} + +.page-toc .guide-toc-section__link { + color: var(--text); + font-weight: 700; +} + +.guide-toc-section__entries, +.guide-toc-root { + display: grid; + gap: 0.02rem; +} + +.page-toc .guide-toc-entry__link { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + padding-left: 1.05rem; +} + +.page-toc .guide-toc-root__link { + color: var(--text-soft); + font-size: 0.67rem; + font-weight: 650; + padding-left: 1.45rem; +} + +.page-toc .guide-toc-member { + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.63rem; + padding-block: 0.22rem; + padding-left: 1.85rem; +} + +.page-toc .guide-toc-member--depth-1 { + padding-left: 2.25rem; +} + +.page-toc .guide-toc-member--depth-2 { + padding-left: 2.65rem; +} + .breadcrumbs { color: var(--muted); display: flex; @@ -664,6 +718,7 @@ h3 { .api-group { margin-top: 5rem; + min-width: 0; } .api-group__heading { @@ -675,11 +730,51 @@ h3 { padding-bottom: 0.8rem; } -.api-group__heading h2 { +.api-group__heading h2, +.api-group__heading h3 { font-size: 1.65rem; margin: 0; } +.guide-header { + border-bottom: 1px solid var(--border); + padding-bottom: 3rem; +} + +.workflow-section__header p { + color: var(--text-soft); + line-height: 1.7; + margin-bottom: 0; + max-width: 45rem; +} + +.workflow-section { + min-width: 0; + padding-top: 6rem; +} + +.workflow-section + .workflow-section { + border-top: 1px solid var(--border); + margin-top: 3rem; +} + +.workflow-section__header { + margin-bottom: 1.25rem; +} + +.workflow-section__header h2 { + font-size: clamp(1.8rem, 3vw, 2.35rem); + margin-bottom: 0.7rem; +} + +.workflow-section__entries { + min-width: 0; +} + +.declaration--core { + padding: 3.75rem 0 4.25rem; +} + .api-group__heading span { color: var(--muted); font-size: 0.72rem; @@ -687,6 +782,7 @@ h3 { .declaration { border-bottom: 1px solid var(--border); + min-width: 0; padding: 2.8rem 0 3.2rem; } @@ -706,7 +802,8 @@ h3 { min-width: 0; } -.declaration__title h3 { +.declaration__title h3, +.declaration__title h4 { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 1.25rem; margin: 0; @@ -736,11 +833,14 @@ h3 { border: 1px solid var(--border); border-radius: 0.65rem; margin: 1rem 0 1.4rem; + max-width: 100%; + min-width: 0; overflow: hidden; position: relative; } .code-block pre { + max-width: 100%; margin: 0; overflow-x: auto; padding: 1.15rem 3.5rem 1.15rem 1.15rem; @@ -848,6 +948,123 @@ h3 { color: var(--muted); } +.usage-section { + margin-top: 5.75rem; + max-width: 100%; + min-width: 0; +} + +.usage-root__description, +.usage-member__description, +.usage-parameters dd { + color: var(--text-soft); + font-size: 0.82rem; + line-height: 1.65; +} + +.usage-root__description p, +.usage-member__description p { + margin: 0; +} + +.usage-roots { + display: grid; + gap: 7rem; + min-width: 0; +} + +.usage-root { + max-width: 100%; + min-width: 0; +} + +.usage-root__header, +.usage-member__header { + align-items: center; + display: flex; + justify-content: space-between; +} + +.usage-root__header h4, +.usage-root__header h5, +.usage-root__header h6 { + font-size: 1.1rem; + margin: 0; +} + +.usage-root__description { + margin-top: 0.6rem; +} + +.usage-members { + display: grid; + gap: 5.5rem; + margin-top: 4.5rem; + min-width: 0; +} + +.usage-members--nested { + gap: 4.5rem; + margin-left: 1rem; + margin-top: 4.5rem; + padding-left: 1rem; +} + +.usage-member { + max-width: 100%; + min-width: 0; + padding: 0; +} + +.usage-member__header h5, +.usage-member__header h6 { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 1.02rem; + margin: 0; + overflow-wrap: anywhere; +} + +.usage-member__header .source-link { + font-size: 0.72rem; +} + +.code-block--usage-signature { + margin: 1.15rem 0 1.2rem; +} + +.code-block--usage-signature pre { + padding-block: 0.75rem; +} + +.usage-member__default { + color: var(--muted); + font-size: 0.75rem; + margin-top: 0.45rem; +} + +.usage-parameters { + display: grid; + gap: 0.5rem; + margin: 0.75rem 0 0; +} + +.usage-parameters > div { + display: grid; + gap: 0.3rem; + grid-template-columns: minmax(8rem, 0.45fr) minmax(0, 1fr); + min-width: 0; +} + +.usage-parameters dt, +.usage-parameters dd { + margin: 0; + min-width: 0; +} + +.usage-parameters code { + overflow-wrap: anywhere; +} + .see-also { margin-top: 1.25rem; } @@ -1034,6 +1251,7 @@ h3 { .reference-hero h1, .module-header h1, + .guide-header h1, .changelog-header h1 { font-size: clamp(2.15rem, 11vw, 3.35rem); } @@ -1059,6 +1277,36 @@ h3 { flex-wrap: wrap; } + .workflow-section { + padding-top: 3.5rem; + } + + .workflow-section + .workflow-section { + margin-top: 1rem; + } + + .usage-section { + margin-top: 4.5rem; + } + + .usage-roots { + gap: 5.5rem; + } + + .usage-members { + gap: 4.5rem; + margin-top: 3.5rem; + } + + .usage-members--nested { + margin-left: 0.35rem; + padding-left: 0.75rem; + } + + .usage-parameters > div { + grid-template-columns: minmax(0, 1fr); + } + .anchor { display: none; } diff --git a/scripts/api-reference-site/generate.mjs b/scripts/api-reference-site/generate.mjs index 58f11bb..bc24d56 100644 --- a/scripts/api-reference-site/generate.mjs +++ b/scripts/api-reference-site/generate.mjs @@ -51,13 +51,17 @@ const readSiteModel = (inputDirectory, config) => { const packageManifestPath = safeResolve(inputDirectory, packageEntry.manifest) const packageDirectory = dirname(packageManifestPath) const packageManifest = readJson(packageManifestPath) - if (packageManifest.schemaVersion !== 4 || !Array.isArray(packageManifest.modules)) { + if (packageManifest.schemaVersion !== 6 || !Array.isArray(packageManifest.modules)) { throw new Error("Unsupported API reference package manifest") } const modules = packageManifest.modules.map((entry) => { const reflection = readJson(safeResolve(packageDirectory, entry.json)) - const api = normalizeApiModule(reflection) + const api = normalizeApiModule( + reflection, + entry.usageSections ?? [], + entry.referenceSections ?? [] + ) const route = moduleRoute(entry.json) const segments = entry.export.replace(/^\.\//, "").split("/") return { @@ -76,6 +80,7 @@ const readSiteModel = (inputDirectory, config) => { revision: dataset.revision, package: packageManifest, modules, + guideModule: modules.find((module) => (module.api.referenceSections ?? []).length > 0), navigation: groupNavigation(modules), changelog } @@ -98,6 +103,9 @@ const groupNavigation = (modules) => { const writeSite = (outputDirectory, site) => { writePage(join(outputDirectory, "index.html"), renderIndexPage(site)) writePage(join(outputDirectory, "changelog", "index.html"), renderChangelogPage(site)) + if (site.guideModule !== undefined) { + writePage(join(outputDirectory, "guide", "index.html"), renderGuidePage(site)) + } for (const module of site.modules) { writePage(join(outputDirectory, module.route, "index.html"), renderModulePage(site, module)) } @@ -203,9 +211,9 @@ export const renderModulePage = (site, module) => {
${escapeHtml(titleCase(group.category))} - ${group.declarations.length} - -