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
32 changes: 32 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
# Changesets

Run `pnpm changeset` for every user-facing change.

## Writing changelog entries

Changeset descriptions are published directly on the documentation website. Write them for library users rather than repository maintainers.

- Lead with the user-visible outcome and name the affected API when useful.
- Keep the entry short and specific. One to three brief paragraphs is usually enough.
- Use separate paragraphs when they make the outcome, motivation, or migration clearer.
- Use inline code for API names, types, and short expressions.
- Include at most one small fenced TypeScript example when an API is added or its usage changes meaningfully.
- For a breaking change, state what changed and show the replacement or migration directly.
- Omit commit hashes, pull request numbers, implementation history, test details, and internal refactoring unless they affect users.

A typical API entry looks like:

````md
Add `Machine.example` for describing the user-visible behavior.

Use it when a short explanation would not make the new calling pattern clear:

```ts
const value = Machine.example(input)
```
````

Prefer a shorter entry without an example for fixes and internal improvements:

```md
Fix resumed machines so nested history is restored before raised events are processed.

This preserves the same observable transition order as a freshly started machine.
```
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:

example:
needs: discover-examples
if: needs.discover-examples.outputs.required == 'true'
strategy:
fail-fast: false
matrix:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
pages: read
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ Every package directly below `examples/` must have a `check` script and a commit

## Pull request conventions

- Add or update a changeset for changes under `src/` or changes to `package.json`.
- Add or update a changeset for changes under `src/` or changes to `package.json`, following the changelog-writing guide in `.changeset/README.md`.
- Fill in the pull request template, including the validation performed and the changeset decision.
28 changes: 25 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,31 @@

### Minor Changes

- b192484: Allow state query helpers to inspect extracted snapshot subtrees, and add
equality-aware `AtomMachine.selectSnapshot` and `selectSnapshotChild`
combinators.
- b192484: Allow `Machine.defineStates` query helpers to inspect an extracted
snapshot subtree. Paths remain absolute and type-safe, but `get`,
`getSnapshot`, and `matches` can now continue from a snapshot selected
earlier instead of requiring the complete root snapshot.

```ts
const readySnapshot = States.getSnapshot(snapshot, "Ready")

if (Option.isSome(readySnapshot)) {
States.get(readySnapshot.value, "Ready.editor")
States.matches(readySnapshot.value, "Ready.editor.Editing")
}

const editorSnapshotAtom = AtomMachine.selectSnapshot(
machineAtom,
"Ready.editor"
)
```

Add equality-aware `AtomMachine.selectSnapshot` and
`AtomMachine.selectSnapshotChild` combinators for reactive consumers that
need the complete logical snapshot subtree instead of only its state value.
The selected atoms retain nested topology, suppress structurally equal
updates, and produce `Option.none()` while the path or invoked child is
inactive.

## 0.6.1

Expand Down
107 changes: 107 additions & 0 deletions scripts/api-reference-site/api-reference-site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
normalizeBasePath,
normalizeGitHubStars,
normalizeOrigin,
parseChangelog,
parseChangeset,
renderChangelogPage,
renderIndexPage,
renderLayout,
renderMarkdown,
renderModulePage,
renderRobots,
renderSitemap,
siteManifest,
Expand All @@ -30,6 +34,22 @@ test("renders documentation prose while escaping source HTML", () => {
assert.doesNotMatch(renderMarkdown("<script>alert(1)</script>"), /<script>/)
})

test("renders fenced code with highlighting and copy controls", () => {
const html = renderMarkdown(`Call the new API:

\`\`\`ts
const value = Machine.example("safe")
\`\`\`

Then reuse \`value\`.`)
assert.match(html, /<p>Call the new API:<\/p>/)
assert.match(html, /class="code-block code-block--markdown"/)
assert.match(html, /aria-label="Copy ts code"/)
assert.match(html, /syntax-keyword">const<\/span>/)
assert.match(html, /syntax-string">&quot;safe&quot;<\/span>/)
assert.match(html, /<p>Then reuse <code>value<\/code>\.<\/p>/)
})

test("assigns deterministic unique anchors to duplicate declarations", () => {
const first = { name: "Machine" }
const second = { name: "Machine" }
Expand Down Expand Up @@ -88,6 +108,92 @@ const site = {
title: "Effect Machine"
}

test("parses Changesets release entries and pending descriptions", () => {
const releases = parseChangelog(`# Package

## 1.2.0

### Minor Changes

- abc1234: Add a typed \`make\` helper.

Preserve inference across multiple lines.

\`\`\`ts
const machine = make()
\`\`\`

### Patch Changes

- def5678: Fix escaped output.
`, new Map([["1.2.0", "2026-08-13"]]))
assert.deepEqual(releases, [{
version: "1.2.0",
date: "2026-08-13",
groups: [{
type: "minor",
entries: [{
description: "Add a typed `make` helper.\n\nPreserve inference across multiple lines.\n\n```ts\nconst machine = make()\n```"
}]
}, {
type: "patch",
entries: [{ description: "Fix escaped output." }]
}]
}])
assert.deepEqual(parseChangeset(`---
"@typeonce/effect-machine": minor
---

Add snapshot selectors.
`, "@typeonce/effect-machine"), { type: "minor", description: "Add snapshot selectors." })
assert.equal(parseChangeset(`---
"another-package": patch
---

Ignore this package.
`, "@typeonce/effect-machine"), undefined)
})

test("renders a navigable changelog with release dates", () => {
const html = renderChangelogPage({
...site,
changelog: [{
version: "1.2.0",
date: "2026-08-13",
groups: [{ type: "minor", entries: [{ description: "Add a feature." }] }]
}]
})
assert.match(html, /class="navigation-changelog is-current"/)
assert.match(html, /id="release-1-2-0">v1\.2\.0/)
assert.match(html, /datetime="2026-08-13">August 13, 2026/)
assert.match(html, /<li><p>Add a feature\.<\/p><\/li>/)
})

test("renders collapsible category navigation with declaration anchors", () => {
const declaration = {
name: "make",
kind: "variable",
description: "Creates a machine.",
examples: [],
see: []
}
const module = {
api: {
declarationCount: 1,
description: "State machine APIs",
groups: [{ category: "constructors", declarations: [declaration] }]
},
export: "./Machine",
label: "Machine",
route: "Machine"
}
const html = renderModulePage({ ...site, modules: [module] }, module)
assert.match(html, /<details class="page-toc__group">/)
assert.match(html, /<summary>[\s\S]*Constructors[\s\S]*<\/summary>/)
assert.match(html, /<a href="#make">make<\/a>/)
assert.match(html, /<h3 id="make">make<\/h3>/)
})

test("renders canonical and social metadata without exposing the internal channel", () => {
const html = renderLayout(site, {
content: '<section class="reference-hero"><div class="eyebrow">API reference</div></section>',
Expand Down Expand Up @@ -162,6 +268,7 @@ test("generates manifest, robots, and sitemap URLs from the deployment base", ()
assert.equal(manifest.icons[2].purpose, "maskable")
assert.match(renderRobots(site), /Sitemap: https:\/\/docs\.example\.com\/docs\/sitemap\.xml/)
assert.match(renderSitemap(site), /<loc>https:\/\/docs\.example\.com\/docs\/Machine\/<\/loc>/)
assert.match(renderSitemap(site), /<loc>https:\/\/docs\.example\.com\/docs\/changelog\/<\/loc>/)
})

test("accepts only pathless HTTPS production origins", () => {
Expand Down
18 changes: 15 additions & 3 deletions scripts/api-reference-site/assets/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,26 @@ searchInput?.addEventListener("input", async () => {
const search = await pagefind.search(query)
const data = await Promise.all(search.results.slice(0, 12).map((result) => result.data()))
if (sequence !== searchSequence) return
searchStatus.textContent = `${search.results.length} result${search.results.length === 1 ? "" : "s"}`
for (const result of data) {
const seen = new Set()
const results = data.flatMap((result) => {
const sections = result.sub_results?.filter((section) => section.url.includes("#")) ?? []
const candidates = sections.length === 0
? [{ title: result.meta.title, url: result.url, excerpt: result.excerpt }]
: sections.map((section) => ({ ...section, title: `${section.title} · ${result.meta.title}` }))
return candidates.filter((candidate) => {
if (seen.has(candidate.url)) return false
seen.add(candidate.url)
return true
})
}).slice(0, 12)
searchStatus.textContent = `${results.length} result${results.length === 1 ? "" : "s"}`
for (const result of results) {
const item = document.createElement("li")
const link = document.createElement("a")
const title = document.createElement("strong")
const excerpt = document.createElement("span")
link.href = result.url
title.textContent = result.meta.title
title.textContent = result.title
excerpt.innerHTML = result.excerpt
link.append(title, excerpt)
item.append(link)
Expand Down
Loading