Skip to content
Open
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
45 changes: 45 additions & 0 deletions .cursor/BUGBOT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# RxJS Bugbot Review Rules

Project-specific PR review instructions for the RxJS 8 monorepo. Default branch is `master`. Use **Yarn 1.22.21** — not npm workspaces.

Reference docs (follow these; do not duplicate in review comments):

- [AGENTS.md](../AGENTS.md) — package map and commands
- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR flow, style, commits, tests
- [.cursor/rules/](./rules/) — scoped authoring rules for agents

## Repository-wide gates

If the PR modifies files under `.nx/cache/**`, `.yarn/cache/**`, `.tshy/**`, `dist/**`, or `**/generated/**` (except intentional release artifacts committed by maintainers), then add a blocking bug titled **"Do not commit build or cache output"** with body: "Revert generated, cache, and dist edits. Edit source files instead."

If the PR modifies `package-lock.json` or uses npm-specific workspace config instead of the repo's Yarn 1 setup, then add a blocking bug titled **"Use Yarn 1.22.21"** with body: "This monorepo uses Yarn 1.22.21. Do not add npm lockfiles or npm workspace changes."

If the PR targets a base branch other than `master` without clear release-maintainer context, then add a blocking bug titled **"Target master by default"** with body: "RxJS contribution PRs should target `ReactiveX/rxjs:master` unless a maintainer is intentionally preparing a release or backport branch."

If the PR adds pipeable operators or operator-like transforms under `packages/observable/**`, then add a blocking bug titled **"Operators belong in packages/rxjs"** with body: "`@rxjs/observable` is core primitives only. Move operator logic to `packages/rxjs/src/internal/operators/`."

If the PR edits API documentation output under `apps/rxjs.dev/` that is generated from JSDoc (dgeni output), then add a blocking bug titled **"Do not edit generated API docs"** with body: "Update JSDoc in `packages/rxjs/src/` and regenerate with `yarn workspace rxjs.dev docs`. Hand-written guides live in `apps/rxjs.dev/content/`."

If the PR changes public exports in `packages/rxjs/src/index.ts` or subpath barrels (`src/ajax/`, `src/fetch/`, `src/testing/`, `src/webSocket/`) without corresponding tests or JSDoc updates in the changed API surface, then add a blocking bug titled **"Public API change missing tests or docs"** with body: "New or changed public API requires marble or unit tests, dtslint when types change, and JSDoc with `@example` (marble PNG when stream-shaped)."

If a non-trivial PR has an empty or vague PR description that does not explain the problem, the chosen fix, and how it was tested, then add a non-blocking bug titled **"PR description missing review context"** with body: "Maintainers repeatedly ask for enough context to judge necessity and scope. Summarize the problem, implementation approach, and test/CI coverage in the PR body."

If the PR adds a new public API surface (operator, creation function, scheduler, subject helper, exported type, or package subpath) without linking an issue, discussion, or clear rationale for why it belongs in core rather than userland, then add a blocking bug titled **"New public API needs core rationale"** with body: "New RxJS API needs explicit maintainer-facing rationale: use case, semantics, naming, compatibility, and why this should live in RxJS core instead of a userland package."

If a PR mixes material library behavior changes with broad formatting-only or whitespace-only edits across unrelated files, then add a non-blocking bug titled **"Split formatting churn from behavior changes"** with body: "Keep mechanical formatting or move-only churn out of behavior PRs so reviewers can see the semantic diff. Put broad formatting changes in a follow-up PR."

If changed Markdown, HTML, or JSDoc adds links to paths that do not exist in this repository, references old repo locations such as `docs_app/`, or uses stale version names that conflict with the current RxJS 8 branch, then add a blocking bug titled **"Docs link or version drift"** with body: "Update links and version references to the current monorepo layout. Do not introduce stale `docs_app/` paths or outdated RxJS version guidance."

If the PR changes GitHub Actions workflows, package scripts, TypeScript config, dependency installation, or lockfile behavior in a way that drops the Node 18/20 CI matrix, skips `rxjs` lint/build/test/dtslint/import/esm checks, or bypasses `rxjs.dev` build/test checks without an explicit maintainer migration note, then add a blocking bug titled **"Preserve RxJS CI coverage"** with body: "CI changes must keep the package and docs checks that catch contribution regressions: Node 18/20, lint, build, unit tests, dtslint, import/esm tests, and rxjs.dev build/test unless a maintainer is intentionally migrating CI."

## Non-blocking guidance

If the only issues are Prettier/ESLint formatting that CI auto-fixes, do not file blocking bugs — mention as a non-blocking note only when the diff clearly violates stated style (single quotes, 140 char width, `import type` preference).

If the PR is an intentional breaking change for RxJS 8 with migration notes in `apps/rxjs.dev/content/deprecations/`, do not flag removal of deprecated APIs as a regression.

Scoped rules for changed paths:

- `packages/rxjs/` → [packages/rxjs/.cursor/BUGBOT.md](../packages/rxjs/.cursor/BUGBOT.md)
- `packages/observable/` → [packages/observable/.cursor/BUGBOT.md](../packages/observable/.cursor/BUGBOT.md)
- `apps/rxjs.dev/` → [apps/rxjs.dev/.cursor/BUGBOT.md](../apps/rxjs.dev/.cursor/BUGBOT.md)
17 changes: 17 additions & 0 deletions .cursor/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": 1,
"hooks": {
"afterFileEdit": [
{
"command": ".cursor/hooks/format-and-lint.sh"
}
],
"beforeShellExecution": [
{
"command": ".cursor/hooks/before-git-commit.sh",
"matcher": "git\\s+commit",
"failClosed": true
}
]
}
}
133 changes: 133 additions & 0 deletions .cursor/hooks/before-git-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Inject conventional-commit guidance before agent git commits.
# Validates extracted messages; blocks invalid commits via permission: deny.

set -euo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
VALIDATOR="$ROOT/.cursor/skills/rxjs-conventional-commits/scripts/validate-message.sh"
SKILL=".cursor/skills/rxjs-conventional-commits/SKILL.md"

input="$(cat)"
command="$(echo "$input" | jq -r '.command // empty')"

# Only git commit (including --amend); ignore other git subcommands.
if [[ ! "$command" =~ (^|[[:space:]])git[[:space:]]+commit([[:space:]]|$) ]]; then
echo '{ "permission": "allow" }'
exit 0
fi

extract_heredoc_message() {
local cmd="$1"

# Single-line HEREDOC: git commit -m "$(cat <<'EOF' body EOF )"
if [[ "$cmd" == *"<<"*"EOF"* ]]; then
local inline
inline="$(printf '%s' "$cmd" | awk '
match($0, /<<[[:space:]]*('\''EOF'\''|"EOF"|EOF)[[:space:]]*/) {
start = RSTART + RLENGTH
rest = substr($0, start)
if (match(rest, /EOF/)) {
body = substr(rest, 1, RSTART - 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", body)
gsub(/\)[[:space:]]*$/, "", body)
if (length(body) > 0) {
print body
}
}
}
')"
if [[ -n "$inline" ]]; then
printf '%s' "$inline"
return 0
fi
fi

# Multi-line HEREDOC (command string contains literal newlines)
if [[ "$cmd" == *$'\n'* && "$cmd" == *"<<"*"EOF"* ]]; then
local multiline
multiline="$(printf '%s\n' "$cmd" | awk '
BEGIN { in_body = 0 }
/cat[[:space:]]+<<[[:space:]]*/ {
in_body = 1
next
}
in_body && (/^EOF[[:space:]]*\)/ || /^EOF[[:space:]]*$/) {
exit
}
in_body {
print
}
')"
if [[ -n "$multiline" ]]; then
printf '%s' "$multiline"
return 0
fi
fi

return 1
}

extract_message() {
local cmd="$1"
local msg=""

if msg="$(extract_heredoc_message "$cmd")"; then
printf '%s' "$msg"
return 0
fi

# Repeated -m "..." / -m '...' (git joins with newlines)
while [[ "$cmd" =~ -m[[:space:]]+ ]]; do
local fragment=""
if [[ "$cmd" =~ -m[[:space:]]+\"(([^\"\\]|\\.)*)\" ]]; then
fragment="${BASH_REMATCH[1]}"
cmd="${cmd/-m \"${BASH_REMATCH[1]}\"/}"
elif [[ "$cmd" =~ -m[[:space:]]+\'([^\']*)\' ]]; then
fragment="${BASH_REMATCH[1]}"
cmd="${cmd/-m \'${BASH_REMATCH[1]}\'/}"
else
break
fi
if [[ -n "$msg" ]]; then
msg+=$'\n'
fi
msg+="$fragment"
done

if [[ -n "$msg" ]]; then
printf '%s' "$msg"
fi
}

deny_invalid_message() {
local validation="$1"
jq -n \
--arg skill "$SKILL" \
--arg validation "$validation" \
'{
permission: "deny",
user_message: "Commit message does not follow RxJS conventional-commit format.",
agent_message: ("Rewrite the commit message per " + $skill + " before committing.\n\nValidation errors:\n" + $validation)
}'
}

message="$(extract_message "$command" || true)"

if [[ -n "$message" && -x "$VALIDATOR" ]]; then
validation_err="$(mktemp)"
if ! "$VALIDATOR" "$message" 2>"$validation_err"; then
validation="$(cat "$validation_err")"
rm -f "$validation_err"
deny_invalid_message "$validation"
exit 0
fi
rm -f "$validation_err"
fi

jq -n \
--arg skill "$SKILL" \
'{
permission: "allow",
agent_message: ("Follow RxJS conventional commits (" + $skill + "): type(scope): subject, imperative lowercase subject, max 100 chars/line. Run git diff first; use a HEREDOC for multi-line messages.")
}'
58 changes: 58 additions & 0 deletions .cursor/hooks/format-and-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Post-edit formatter: runs eslint --fix and prettier --write on agent-edited files.
# Always exits 0 so lint/format issues never block the agent.

set -uo pipefail

ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
ESLINT="$ROOT/node_modules/.bin/eslint"
PRETTIER="$ROOT/node_modules/.bin/prettier"

input="$(cat)"
file_path="$(echo "$input" | jq -r '.file_path // empty')"

if [[ -z "$file_path" ]]; then
exit 0
fi

# Normalize to an absolute path under the repo when possible.
if [[ "$file_path" != /* ]]; then
file_path="$ROOT/$file_path"
fi

# Only process files inside this repository.
case "$file_path" in
"$ROOT"/*) ;;
*) exit 0 ;;
esac

rel="${file_path#"$ROOT"/}"

# Skip generated, vendored, and non-source paths.
case "$rel" in
node_modules/* | dist/* | .yarn/* | yarn.lock | package-lock.json | *.min.js | *.min.css)
exit 0
;;
esac

run_quietly() {
"$@" >/dev/null 2>&1 || true
}

case "$rel" in
*.ts | *.tsx | *.js | *.jsx)
if [[ -x "$ESLINT" ]]; then
run_quietly "$ESLINT" --fix "$file_path"
fi
;;
esac

case "$rel" in
*.ts | *.tsx | *.js | *.jsx | *.css | *.md | *.json | *.yml | *.yaml)
if [[ -x "$PRETTIER" ]]; then
run_quietly "$PRETTIER" --write "$file_path"
fi
;;
esac

exit 0
8 changes: 8 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"firebase": {
"command": "npx",
"args": ["-y", "firebase-tools@latest", "mcp", "--dir", "${workspaceFolder}/apps/rxjs.dev", "--only", "hosting"]
}
}
}
55 changes: 55 additions & 0 deletions .cursor/rules/rxjs-core-internals.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
description: Non-operator RxJS internals — observables, subjects, schedulers, ajax, util
globs: packages/rxjs/src/internal/*.ts,packages/rxjs/src/internal/observable/**,packages/rxjs/src/internal/scheduler/**,packages/rxjs/src/internal/ajax/**,packages/rxjs/src/internal/util/**,packages/rxjs/src/internal/scheduled/**,packages/rxjs/src/internal/testing/**,packages/rxjs/src/ajax/**,packages/rxjs/src/fetch/**,packages/rxjs/src/testing/**,packages/rxjs/src/webSocket/**
alwaysApply: false
---

# RxJS Core Internals

Not pipeable operators. For operator patterns, see `rxjs-operators`.

## Categories

| Area | Path | Examples |
|------|------|----------|
| Creation functions | `internal/observable/` | `of`, `from`, `defer`, `timer` |
| Subjects | `internal/*Subject.ts` | `BehaviorSubject`, `ReplaySubject` |
| Schedulers | `internal/scheduler/` | `async`, `queue`, `animationFrame` |
| Ajax / WebSocket | `internal/ajax/`, `webSocket/` | HTTP helpers, socket wrapper |
| Utilities | `internal/util/` | errors, type guards, helpers |
| Testing | `internal/testing/`, `src/testing/` | `TestScheduler`, test doubles |

## Sibling references

Before inventing patterns, read the nearest existing implementation and spec pair:

| Area | Implementation | Spec |
|------|----------------|------|
| Creation | `internal/observable/defer.ts`, `timer.ts` | `spec/observables/defer-spec.ts` |
| Schedulers | `internal/scheduler/AsyncScheduler.ts`, `async.ts`, `AsyncAction.ts` | `spec/schedulers/QueueScheduler-spec.ts` |
| Timer limits | `internal/util/maxTimerDelay.ts` (when present), `AsyncAction.schedule` | `spec/observables/timer-spec.ts`, `spec/operators/delay-spec.ts` |
| Subjects | `internal/BehaviorSubject.ts` | `spec/subjects/BehaviorSubject-spec.ts` |
| Ajax | `internal/ajax/ajax.ts` | `spec/ajax/index-spec.ts` |

Real timer validation for `asyncScheduler` belongs in **`AsyncAction.schedule`** (the `setInterval` choke point), not only in individual operators.

## Implementation

- Creation functions return `Observable` directly (not `OperatorFunction`)
- Subjects extend `Subject`; schedulers implement `Scheduler` / `SchedulerAction`
- Use `@rxjs/observable` primitives; keep operator logic out of this layer

## Exports

- Main API: `packages/rxjs/src/index.ts`
- Subpath barrels: `src/ajax/`, `src/fetch/`, `src/testing/`, `src/webSocket/`

## Tests

- `spec/observables/`, `spec/subjects/`, `spec/schedulers/`, `spec/ajax/`, `spec/websocket/`
- Marble tests where timing matters; synchronous Chai tests where appropriate
- Not the operator-only canonical-case checklist — match patterns in sibling specs

## JSDoc

Public API still needs JSDoc with `@example`. Marble PNG diagrams when the API is stream-shaped.
34 changes: 34 additions & 0 deletions .cursor/rules/rxjs-dev-site.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
description: rxjs.dev docs site — dgeni API docs vs hand-written guides
globs: apps/rxjs.dev/**
alwaysApply: false
---

# rxjs.dev Docs Site

## Two doc sources

| Source | Location | How it updates |
|--------|----------|----------------|
| API docs | Generated from JSDoc in `packages/rxjs/src/` | Edit source JSDoc; run `yarn workspace rxjs.dev docs` |
| Guides & deprecations | `apps/rxjs.dev/content/` | Edit markdown directly |

Never edit generated API JSON output.

## When changing public API

Update JSDoc in `packages/rxjs/src/` (informal span, marble PNG, `@example`). Guides in `content/` only when adding narrative docs (e.g. new operator category in `content/guide/operators.md`).

## App stack

- Angular 13 docs application
- Component tests: Jasmine/Karma (`yarn workspace rxjs.dev test`)
- Doc generation: dgeni (`yarn workspace rxjs.dev docs`)

## Dev

```sh
yarn workspace rxjs.dev start
```

Deep reference: [apps/rxjs.dev/README.md](../../apps/rxjs.dev/README.md)
Loading
Loading