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
37 changes: 37 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: preview

on: [pull_request]

permissions:
contents: read

jobs:
preview:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Get Version
id: vars
run: echo "version=$(git describe --abbrev=0 --tags 2>/dev/null | sed 's/^v//' || echo '0.0.0')-pr+$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: https://registry.npmjs.com

- name: Build NPM
run: deno task build:npm ${{steps.vars.outputs.version}}

- name: Publish Preview Versions
run: npx pkg-pr-new publish './build/npm'
109 changes: 109 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Publish

on:
push:
tags:
- "v*"

permissions:
contents: read
id-token: write

jobs:
verify-jsr:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Get Version
id: vars
run: echo "version=$(echo ${{github.ref_name}} | sed 's/^v//')" >> "$GITHUB_OUTPUT"

- name: Build JSR
run: deno task build:jsr ${{steps.vars.outputs.version}}

- name: dry run publish
run: deno publish --dry-run --allow-dirty

verify-npm:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Get Version
id: vars
run: echo "version=$(echo ${{github.ref_name}} | sed 's/^v//')" >> "$GITHUB_OUTPUT"

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Build NPM
run: deno task build:npm ${{steps.vars.outputs.version}}

- name: dry run publish
run: npm publish --dry-run --tag=verify
working-directory: ./build/npm

- name: upload build
uses: actions/upload-artifact@v4
with:
name: npm-build
path: ./build/npm

publish-npm:
needs: [verify-jsr, verify-npm]
runs-on: ubuntu-latest
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: https://registry.npmjs.com

- name: download build
uses: actions/download-artifact@v4
with:
name: npm-build
path: ./build/npm

- name: Publish NPM
run: npm publish --access=public --tag=latest
working-directory: ./build/npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

publish-jsr:
needs: [verify-jsr, verify-npm]
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Get Version
id: vars
run: echo "version=$(echo ${{github.ref_name}} | sed 's/^v//')" >> "$GITHUB_OUTPUT"

- name: Build JSR
run: deno task build:jsr ${{steps.vars.outputs.version}}

- name: Publish JSR
run: deno publish --allow-dirty --token=${{ secrets.JSR_TOKEN }}
74 changes: 74 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Verify

on:
push:
branches: main
pull_request:
branches: main

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: format
run: deno fmt --check

- name: lint
run: deno lint

- name: test
run: deno task test

jsr:
needs: test
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Build JSR
run: deno task build:jsr 0.0.0-verify.0

- name: dry run publish
run: deno publish --dry-run --allow-dirty

npm:
needs: test
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: setup deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x

- name: Build
run: deno task build:npm 0.0.0-verify.0

- name: dry run publish
run: npm publish --dry-run --tag=verify
working-directory: ./build/npm
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/.agent-shell/
/build/
/**/node_modules/
hack.ts
66 changes: 30 additions & 36 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,55 @@

## Spec-driven development

- Specs in `specs/` are the source of truth. Code conforms to specs,
not the other way around.
- Specs in `specs/` are the source of truth. Code conforms to specs, not the
other way around.

- Never add, remove, or change a public API (interfaces, operations,
context APIs) in code without first updating the relevant spec and
getting explicit approval from the user. This includes changes to
`Node`, `Tree`, `FreedomApi`, `DispatchApi`, `FocusApi`, and any
future spec'd interfaces.
- Never add, remove, or change a public API (interfaces, operations, context
APIs) in code without first updating the relevant spec and getting explicit
approval from the user. This includes changes to `Node`, `Tree`, `FreedomApi`,
`DispatchApi`, `FocusApi`, and any future spec'd interfaces.

- The workflow is: propose the spec change, wait for approval,
then implement. Do not combine spec changes with implementation
in a single step.
- The workflow is: propose the spec change, wait for approval, then implement.
Do not combine spec changes with implementation in a single step.

## Effection patterns

- Never use `sleep(0)` for synchronization. If you need to coordinate
between tasks, use `withResolvers()` from Effection.
- Never use `sleep(0)` for synchronization. If you need to coordinate between
tasks, use `withResolvers()` from Effection.

- Structured concurrency handles cleanup. Do not add explicit "alive"
guards, flags, or checks for whether a scope has been destroyed.
When a scope exits, its signals are inert and its tasks are halted.
- Structured concurrency handles cleanup. Do not add explicit "alive" guards,
flags, or checks for whether a scope has been destroyed. When a scope exits,
its signals are inert and its tasks are halted.

- Do not reimplement middleware. If the spec says `createApi`, use
`createApi` and its `around()` method. Do not maintain a parallel
list of handlers.
- Do not reimplement middleware. If the spec says `createApi`, use `createApi`
and its `around()` method. Do not maintain a parallel list of handlers.

- When delegating `[Symbol.iterator]`, bind directly:
`[Symbol.iterator]: source[Symbol.iterator]` — not a wrapper
generator.
`[Symbol.iterator]: source[Symbol.iterator]` — not a wrapper generator.

- Effection methods are pre-bound. Do not use `.bind()` when
assigning them — just assign directly: `child.remove = task.halt`.
- Effection methods are pre-bound. Do not use `.bind()` when assigning them —
just assign directly: `child.remove = task.halt`.

## Naming conventions

- API interfaces are named for the domain: `Freedom`, `Dispatch`.
- API constants are the interface name suffixed with `Api`:
`FreedomApi`, `DispatchApi`.
- API constants are the interface name suffixed with `Api`: `FreedomApi`,
`DispatchApi`.
- Always export both the interface and the API constant.
- Effection resources use the `useX()` naming convention, not
`createX()`. For example: `useTree()`, not `createTree()`.
- Effection resources use the `useX()` naming convention, not `createX()`. For
example: `useTree()`, not `createTree()`.

## State

- Do not use module-level mutable state (counters, maps, etc.).
Scope state to the resource or tree that owns it.
- Do not use module-level mutable state (counters, maps, etc.). Scope state to
the resource or tree that owns it.

## Testing

- Tests should not need to know internal IDs. If a test needs a
node reference, get it from the tree structure or from the return
value of `append`.
- Use `node.eval()` to run operations in a node's scope from tests.
Do not rely on component bodies having run by the time `createTree`
returns.
- Tests should not need to know internal IDs. If a test needs a node reference,
get it from the tree structure or from the return value of `append`.
- Use `node.eval()` to run operations in a node's scope from tests. Do not rely
on component bodies having run by the time `createTree` returns.
- Each test file tests exactly one spec. `freedom.test.ts` tests
`freedom-spec.md`. `focus.test.ts` tests `freedom-focus-spec.md`.
Do not put tests for one spec into another spec's test file.
`freedom-spec.md`. `focus.test.ts` tests `freedom-focus-spec.md`. Do not put
tests for one spec into another spec's test file.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Freedom

A general-purpose abstract component tree built on [Effection](https://frontside.com/effection) structured concurrency. Freedom ("free DOM") maintains a tree of long-lived, stateful component nodes where each node is an Effection resource with a scope, a JSON-like property bag, and ordered children. It is designed to accept a firehose of events of any type through a single synchronous `dispatch()` entry point, and emit change notifications on an output stream for renderers to consume.
A general-purpose abstract component tree built on
[Effection](https://frontside.com/effection) structured concurrency. Freedom
("free DOM") maintains a tree of long-lived, stateful component nodes where each
node is an Effection resource with a scope, a JSON-like property bag, and
ordered children. It is designed to accept a firehose of events of any type
through a single synchronous `dispatch()` entry point, and emit change
notifications on an output stream for renderers to consume.

## Specs

Expand All @@ -9,11 +15,18 @@ A general-purpose abstract component tree built on [Effection](https://frontside

## Extension Modules

Freedom is extensible through extension modules — operations installed by the root component that add capabilities to the tree using Freedom's context APIs. Extensions use middleware interception, scoped evaluation, and the property bag to layer behavior without modifying the core.
Freedom is extensible through extension modules — operations installed by the
root component that add capabilities to the tree using Freedom's context APIs.
Extensions use middleware interception, scoped evaluation, and the property bag
to layer behavior without modifying the core.

### Focus

The [Focus extension](specs/freedom-focus-spec.md) tracks which node in the tree is currently receiving input. Focus state is observable as a regular node property (`node.props.focused`), and the focus chain is derived from the tree by depth-first traversal. See the [research summary](research/focus.md) for background on focus management across UI paradigms.
The [Focus extension](specs/freedom-focus-spec.md) tracks which node in the tree
is currently receiving input. Focus state is observable as a regular node
property (`node.props.focused`), and the focus chain is derived from the tree by
depth-first traversal. See the [research summary](research/focus.md) for
background on focus management across UI paradigms.

Install focus in the root component:

Expand Down
8 changes: 6 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"publish": { "include": ["lib", "mod.ts", "README.md"] },
"lock": false,
"tasks": {
"test": "deno test"
"test": "deno test",
"build:npm": "deno run -A tasks/build-npm.ts",
"build:jsr": "deno run -A tasks/build-jsr.ts"
},
"lint": {
"rules": { "exclude": ["prefer-const", "require-yield"] },
"exclude": ["build"],
"plugins": ["lint/prefer-let.ts"]
},
"fmt": {},
Expand All @@ -20,7 +23,8 @@
"effection": "jsr:@effection/effection@4.1.0-alpha.7",
"effection/experimental": "jsr:@effection/effection@4.1.0-alpha.7/experimental",
"@std/testing": "jsr:@std/testing@1",
"@std/expect": "jsr:@std/expect@1"
"@std/expect": "jsr:@std/expect@1",
"dnt": "jsr:@deno/dnt@0.42.3"
},
"version": "0.0.0"
}
6 changes: 5 additions & 1 deletion lib/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ function focusChain(node: Node): Node[] {
return result;
}

function* setFocused(target: Node, value: boolean, self: NodeImpl): Operation<void> {
function* setFocused(
target: Node,
value: boolean,
self: NodeImpl,
): Operation<void> {
if (target === self) {
yield* set("focused", value);
} else {
Expand Down
Loading
Loading