As this project's AI coding tool, you must follow the additional conventions below, in addition to the built-in functions.
Ripple is a TypeScript UI framework that combines the best parts of React, Solid,
and Svelte. Created by Dominic Gannaway (@trueadm),
Ripple is designed to be JS/TS-first with its own .ripple file extension that
fully supports TypeScript.
For comprehensive Ripple syntax, components, reactivity, and API documentation, see:
- website/public/llms.txt - Full LLM-optimized documentation
- README.md - Project overview and quick start
- CONTRIBUTING.md - Contribution guidelines
This project uses RuleSync to maintain a
single source of truth for AI agent instructions. The canonical rules are in
.rulesync/rules/, which are automatically generated to tool-specific locations:
| Agent | Generated File |
|---|---|
| Claude Code | CLAUDE.md |
| GitHub Copilot | .github/copilot-instructions.md |
| Cursor | .cursor/rules/project.mdc |
| Gemini CLI | GEMINI.md |
| AGENTS.md | AGENTS.md |
To regenerate after editing .rulesync/rules/:
pnpm rules:generateThis runs automatically on pnpm install via the prepare script.
This is a pnpm monorepo. Key packages are marked with *.
packages/
├── ripple/* # Core framework
│ └── src/
│ ├── compiler/ # Compilation pipeline (see Compiler Architecture)
│ │ ├── phases/
│ │ │ ├── 1-parse/ # Acorn-based parser with RipplePlugin
│ │ │ ├── 2-analyze/ # Scope analysis, CSS pruning, validation
│ │ │ └── 3-transform/# Client/server code generation
│ │ ├── scope.js # Scope and binding management
│ │ ├── types/ # AST type definitions
│ │ └── utils.js # Compiler utilities
│ ├── runtime/ # Runtime library (see Runtime Architecture)
│ │ ├── internal/
│ │ │ ├── client/ # DOM operations, reactivity, events
│ │ │ └── server/ # SSR string generation
│ │ ├── index-client.js # Client entry (browser)
│ │ └── index-server.js # Server entry (SSR)
│ └── server/ # Server-side rendering utilities
├── language-server/* # LSP implementation via Volar framework
├── vscode-plugin/* # VS Code extension (uses language-server)
├── typescript-plugin/* # TypeScript language service plugin
├── eslint-plugin/* # ESLint rules for Ripple
├── eslint-parser/* # ESLint parser for .ripple files
├── prettier-plugin/* # Prettier formatting support
├── vite-plugin/* # Vite build integration
├── rollup-plugin/ # Rollup build integration
├── cli/* # CLI tool (@ripple-ts/cli)
├── create-ripple/ # Project scaffolding (npx create-ripple)
├── compat-react/* # React interoperability layer
├── tree-sitter/* # Tree-sitter grammar for syntax highlighting
├── intellij-plugin/ # IntelliJ/WebStorm support
├── nvim-plugin/ # Neovim support
├── sublime-text-plugin/ # Sublime Text support
├── zed-plugin/ # Zed editor support
└── textmate/ # TextMate grammar (shared by editors)
playground/ # Development playground
website/ # Documentation website
templates/ # Project templates (basic, etc.)
scripts/ # Build and maintenance scripts
The compiler transforms .ripple files through three phases:
Source Code (.ripple) → Parse → Analyze → Transform → Output (JS + CSS)
Parser: Acorn extended with @sveltejs/acorn-typescript and custom
RipplePlugin
Ripple-specific syntax handled:
componentkeyword for component declarations- JSX with special handling for
@tracked expressions #serverblocks for server-only code#ripple[](RippleArray shorthand),#ripple{}(RippleObject shorthand),#ripple.map()(RippleMap),#ripple.set()(RippleSet),#ripple.array()(RippleArray),#ripple.object()(RippleObject),#ripple.url()(RippleURL),#ripple.urlSearchParams()(RippleURLSearchParams),#ripple.Date()(RippleDate),#ripple.Context()(RippleContext),#ripple.mediaQuery()(MediaQuery)#ripple.track()(track()),#ripple.trackSplit()(trackSplit())#ripple.untrack()(untrack()),#ripple.effect()(effect()),#styleidentifier for scoped CSS classes
Output: ESTree-compatible AST with Ripple extensions
| File | Purpose |
|---|---|
index.js |
Main analysis orchestration |
css-analyze.js |
CSS selector analysis, :global() handling |
prune.js |
Remove unused CSS rules based on template usage |
validation.js |
HTML nesting validation |
Key operations:
- Scope creation:
scope.jscreates scope chains tracking bindings (import, prop, let, const, function, component, for_pattern) - Reactivity analysis: Marks tracked expressions, derives tracking metadata
- CSS scoping: Hash-based class names via
CSS_HASH_IDENTIFIER #styleanalysis: Validates usage context, collects referenced classes, cross-checks against standalone CSS selectors- Server block analysis: Tracks exports from
#serverblocks
Client transform (client/index.js):
- Generates runtime calls:
_$_.render(),_$_.if(),_$_.for(),_$_.switch(), etc. - Creates template strings for static HTML
- Sets up event delegation
- Injects CSS hash for scoped styles
Server transform (server/index.js):
- Generates string concatenation for SSR output
- Handles
#serverblock code execution - Registers CSS for hydration
- Wraps control flow blocks with hydration comment markers
The same .ripple module produces different output depending on the compilation
mode, controlled by options.mode in the compiler:
// compiler/index.js
const result =
options.mode === 'server'
? transform_server(filename, source, analysis, options?.minify_css ?? false)
: transform_client(
filename,
source,
analysis,
false,
options?.minify_css ?? false,
);| Aspect | Client Transform | Server Transform |
|---|---|---|
| Output | Runtime calls (_$_.render(), _$_.if()) |
String concatenation (__output.push()) |
| Templates | DOM template literals, cloneNode() |
Escaped HTML strings |
| Reactivity | Block scheduling, dirty checking | Immediate execution, no scheduling |
| Control flow | Creates branch blocks, DOM diffing | Wraps with <!--[-->/<!--]--> markers |
| Events | Delegation setup (_$_.delegate()) |
Omitted entirely |
| CSS | Injects hash for scoping | Registers CSS hash via register_css() |
Vite plugin compiles modules twice for SSR apps - once with mode: 'client'
and once with mode: 'server'.
| Node Type | Description |
|---|---|
Component |
Component declaration with id, params, body, css |
Element |
HTML/SVG element with id, attributes, children |
Text |
Text node wrapping an expression |
ServerBlock |
#server { ... } block with exports tracking |
TrackedExpression |
@expression tracked reactive value |
RippleArrayExpression |
#[...] tracked array literal |
RippleObjectExpression |
#{...} tracked object literal |
Attribute |
Element attribute with name, value, shorthand |
RefAttribute |
ref={...} reference binding |
SpreadAttribute |
{...props} spread |
StyleIdentifier |
#style compile-time identifier for scoped CSS classes |
CSS.StyleSheet |
Parsed CSS with hash for scoping |
| Module | Responsibility |
|---|---|
runtime.js |
Core reactivity: tracked(), derived(), get(), set(), block scheduling |
blocks.js |
Block creation: render(), branch(), effect(), root(), destroy_block() |
render.js |
DOM operations: set_text(), set_class(), set_style(), set_attribute() |
template.js |
Template instantiation: template(), append(), assign_nodes() |
operations.js |
DOM traversal: child(), sibling(), create_text() |
events.js |
Event handling: event(), delegate(), event propagation |
hydration.js |
SSR hydration: hydrating, hydrate_node, hydrate_next() |
bindings.js |
Two-way bindings for form elements |
context.js |
Context API implementation |
| Block | File | Purpose |
|---|---|---|
if_block |
if.js |
Conditional rendering with branch switching |
for_block |
for.js |
List rendering with reconciliation (ref-based or keyed) |
switch_block |
switch.js |
Multi-branch rendering |
try_block |
try.js |
Error boundaries + async suspense |
composite |
composite.js |
Dynamic component rendering (<@Component />) |
portal |
portal.js |
Render children to different DOM location |
Core concepts:
tracked(value, block)- Creates a tracked reactive value (Tracked<V>)derived(fn, block)- Creates a computed/derived valueget(tracked)- Reads value, registers dependencyset(tracked, value)- Updates value, schedules updates
Implementation details:
- Dependencies tracked via linked list structure:
{ c, t, n }(consumer, tracked, next) - Dirty checking with clock-based versioning
- Block flags in
constants.js:ROOT_BLOCK,RENDER_BLOCK,EFFECT_BLOCK,BRANCH_BLOCK, etc.
| Collection | File | Description |
|---|---|---|
RippleArray |
array.js |
Fully reactive array with all Array methods |
RippleObject |
object.js |
Shallow reactive object |
RippleMap |
map.js |
Reactive Map |
RippleSet |
set.js |
Reactive Set |
RippleDate |
date.js |
Reactive Date |
- String-based output via
Outputclass (concatenatesheadandbody) - Simplified reactivity (no block scheduling, immediate execution)
- CSS registration for hydration markers
- Escape utilities for safe HTML output
Hydration allows the client to "adopt" server-rendered HTML without re-rendering, using comment markers to identify dynamic regions.
Comment Markers (inserted by server transform):
| Marker | Constant | Purpose |
|---|---|---|
<!--[--> |
HYDRATION_START |
Opens a dynamic block (if, for, switch, try) |
<!--]--> |
HYDRATION_END |
Closes a dynamic block |
<!--[!--> |
HYDRATION_ELSE |
Marks else/fallback branch boundary |
Server-side generation:
// Server transform wraps control flow with markers
__output.push('<!--[-->'); // HYDRATION_START
// ... render content ...
__output.push('<!--]-->'); // HYDRATION_ENDClient-side hydration
(packages/ripple/src/runtime/internal/client/hydration.js):
export let hydrating = false; // True during hydration phase
export let hydrate_node = null; // Current DOM node being hydratedKey hydration functions:
| Function | Purpose |
|---|---|
set_hydrating(value) |
Enable/disable hydration mode |
set_hydrate_node(node) |
Set the current node pointer |
hydrate_next() |
Advance to next sibling node |
pop(node) |
Reset hydrate_node after mounting |
Hydration flow:
- Server renders HTML with
<!--[-->/<!--]-->markers around dynamic blocks - Client receives HTML,
hydrating = trueis set - Runtime walks DOM using
hydrate_node, matching structure to component tree - Instead of creating elements, runtime "claims" existing DOM nodes
- Comment markers guide block boundary detection
- After hydration completes,
hydratingis set back tofalse
Built on Volar framework with TypeScript integration.
| Plugin | File | Purpose |
|---|---|---|
| Completion | completionPlugin.js |
Auto-completion for Ripple syntax |
| Definition | definitionPlugin.js |
Go-to-definition |
| Hover | hoverPlugin.js |
Hover information |
| Diagnostics | compileErrorDiagnosticPlugin.js |
Compile-time error diagnostics |
| TS Diagnostics | typescriptDiagnosticPlugin.js |
TypeScript diagnostic filtering |
| Auto-insert | autoInsertPlugin.js |
Auto-insert completions |
| Highlight | documentHighlightPlugin.js |
Document highlights |
Integration: Uses @ripple-ts/typescript-plugin for TypeScript language
service.
All editor plugins use @ripple-ts/language-server internally:
| Editor | Package | Notes |
|---|---|---|
| VS Code | vscode-plugin/ |
Primary development target |
| IntelliJ/WebStorm | intellij-plugin/ |
TextMate syntax + LSP via LSP4IJ |
| Neovim | nvim-plugin/ |
Tree-sitter + LSP |
| Sublime Text | sublime-text-plugin/ |
LSP package |
| Zed | zed-plugin/ |
Tree-sitter queries |
Tree-sitter queries: Located in packages/tree-sitter/queries/, copied to
nvim/zed plugins via pnpm copy-tree-sitter-queries.
CRITICAL: Use pnpm for all package management. Do NOT use npm or yarn.
For user-facing changes, add a changeset before committing:
pnpm changesetThis creates a markdown file in .changeset/ describing the change. Select
affected packages and semver bump type (patch/minor/major). The file is committed
with your changes.
Add a changeset for: bug fixes, new features, breaking changes, API changes.
Skip changesets for: docs-only, internal refactoring, tests, CI/tooling.
After making changes, run these commands:
# Install dependencies (if needed)
pnpm install
# Format code with Prettier
pnpm format
# Check formatting without changes
pnpm format:check
# Run all tests
pnpm test
# Run specific test project
pnpm test --project ripple-client
pnpm test --project ripple-server
pnpm test --project eslint-plugin
pnpm test --project prettier-plugin| Project | Tests | Environment |
|---|---|---|
ripple-client |
packages/ripple/tests/client/**/*.test.ripple |
jsdom |
ripple-server |
packages/ripple/tests/server/**/*.test.ripple |
node |
ripple-hydration |
packages/ripple/tests/hydration/**/*.test.js |
jsdom |
eslint-plugin |
packages/eslint-plugin/tests/**/*.test.ts |
jsdom |
eslint-parser |
packages/eslint-parser/tests/**/*.test.ts |
jsdom |
prettier-plugin |
packages/prettier-plugin/src/*.test.js |
jsdom |
cli |
packages/cli/tests/**/*.test.js |
jsdom |
compat-react |
packages/compat-react/tests/**/*.test.ripple |
jsdom |
Ripple test files (.test.ripple):
Test files are valid Ripple modules that export a default test component. The Vite plugin transforms them before Vitest runs:
// Example: packages/ripple/tests/client/reactivity.test.ripple
import { describe, it, expect } from 'vitest';
component default() {
describe('tracked', () => {
it('updates when value changes', async () => {
let count = #ripple.track(0);
// test implementation
});
});
}
Setup files (packages/ripple/tests/):
| File | Purpose |
|---|---|
setup-client.js |
Client test setup: DOM utilities, flush helpers |
setup-server.js |
Server test setup: Output class, render helpers |
Hydration tests (packages/ripple/tests/hydration/):
Hydration tests verify client/server output consistency:
- Server compiles and renders to HTML string with hydration markers
- Client receives pre-rendered HTML, sets
hydrating = true - Client walks DOM, claiming existing nodes instead of creating new ones
- Tests verify final DOM matches expected state
// Typical hydration test pattern
const server_html = render_server(Component); // With <!--[--> markers
container.innerHTML = server_html;
hydrate(Component, container); // Claims existing nodes
expect(container.innerHTML).toBe(expected);cd playground
pnpm dev # Start dev server (Vite)
pnpm lint # Lint playground codepnpm is required (engines in package.json enforces this). Do NOT use npm or
yarn.
- Internal code: JavaScript (
.js) with JSDoc type annotations — NOT TypeScript - Type definitions: TypeScript
.d.tsfiles intypes/directories for public API - JSDoc imports: Use
@importsyntax at top of file:/** @import { Block, Tracked, Derived } from '#client' */ /** @import * as AST from 'estree' */
- JSDoc annotations: Use
@param,@returns,@typefor all functions:/** * @param {Block} block - The block to destroy * @returns {void} */ export function destroy_block(block) { ... }
| Context | Style | Examples |
|---|---|---|
| Variables | snake_case |
active_block, is_mutating_allowed |
| Functions | snake_case |
create_scopes, set_active_block |
| Constants | SCREAMING_SNAKE_CASE |
ROOT_BLOCK, FLUSH_MICROTASK, DERIVED |
| Files | kebab-case |
css-analyze.js, source-map-utils.js |
| Component files | PascalCase |
Button.ripple, TodoList.ripple |
| Classes | PascalCase |
Scope, RippleArray, Output |
| Type parameters | Single uppercase | V in Tracked<V>, T in generics |
In performance-critical runtime code, short property names are used to minimize bundle size:
// Block structure uses short names
block.p; // parent
block.t; // teardown function
block.d; // dependencies
block.f; // flags
block.s; // state
block.c; // context- Consistency: Look for similar implementations before adding new code
- No abbreviations in variable names (except hot path optimizations above)
- Prefer
constoverletwhen value won't be reassigned - Use
varonly in specific runtime hot paths for performance - Comments: Add comments for complex logic, not obvious code
- Parser changes go in
phases/1-parse/, modifyRipplePluginfor new syntax - Scope-related changes in
scope.js- track bindings with appropriatekind - CSS changes:
css-analyze.jsfor parsing,prune.jsfor dead code elimination - Code generation: separate files for
client/andserver/transforms
- Reactivity:
runtime.jsis the core, understandtracked()/derived()/get()/set() - New control flow: add to both client (
internal/client/) and may need server support - DOM operations:
render.jsfor attribute/text updates,operations.jsfor traversal - Events: delegation in
events.js, checkDELEGATED_EVENTSconstant
- Language server plugins in
packages/language-server/src/ - VS Code extension entry:
packages/vscode-plugin/src/extension.js - TypeScript plugin:
packages/typescript-plugin/src/for IDE integration
The Prettier plugin (packages/prettier-plugin/src/index.js) formats .ripple
files using AST-based formatting, not string manipulation.
The plugin exports three objects required by Prettier:
| Export | Purpose |
|---|---|
languages |
Declares .ripple extension and parser name |
parsers |
Uses Ripple's compiler (parse()) to create ESTree-compatible AST |
printers |
Contains print, embed, and getVisitorKeys functions |
AST-based approach:
- Parser produces ESTree AST with Ripple extensions (Component, Element, TrackedExpression, etc.)
- Printer recursively walks AST nodes via
printRippleNode()switch statement - Uses Prettier's
doc.buildersAPI (concat,join,group,indent,line,hardline,softline,ifBreak)
Comments are attached to AST nodes and printed via three mechanisms:
| Comment Type | Property | Handling |
|---|---|---|
| Leading comments | node.leadingComments |
Printed before node content |
| Trailing comments | node.trailingComments |
Inline via lineSuffix() or on next line |
| Inner comments | node.innerComments |
Printed inside empty blocks/elements |
Element-level comment helpers:
getElementLeadingComments(node)- extracts comments for JSX elementscreateElementLevelCommentParts(comments)- formats with proper spacing
Prettier options are accessed from the options parameter:
| Option | Helper function | Usage |
|---|---|---|
singleQuote |
formatStringLiteral() |
Quote style for string literals |
jsxSingleQuote |
— | Quote style for JSX attribute values |
semi |
semi() |
Semicolon insertion |
trailingComma |
shouldPrintComma() |
Trailing commas in arrays/objects |
useTabs / tabWidth |
createIndent() |
Indentation style |
singleAttributePerLine |
— | JSX attribute line breaking |
bracketSameLine |
— | JSX closing bracket position |
The args parameter passes context for conditional formatting:
// Examples of context flags
{
isInAttribute: true;
} // Compact object formatting in attributes
{
isInArray: true;
} // Array element context
{
allowInlineObject: true;
} // Allow single-line objects
{
isConditionalTest: true;
} // Binary/logical in conditional test
{
suppressLeadingComments: true;
} // Skip comment printingWhen encountering /* Unknown: NodeType */ in formatter output:
- Identify the missing node type from the comment (e.g.,
TSDeclareFunction) - Add a case in the
printRippleNodeswitch statement:case 'TSDeclareFunction': nodeContent = printTSDeclareFunction(node, path, options, print); break;
- Implement the print function following existing patterns (see
printFunctionDeclarationas reference) - Add a test in
packages/prettier-plugin/src/index.test.js
- Use
path.call(print, 'childNode')to recursively print child nodes - Use
concat([...])to join parts,group()for line breaking - Check
node.typeParameters,node.returnTypefor TypeScript annotations - All functions use JSDoc type annotations with proper types (no
any/unknown)
- Client tests: create
.test.ripplefiles inpackages/ripple/tests/client/ - Server tests: create
.test.ripplefiles inpackages/ripple/tests/server/ - Use
setup-client.js/setup-server.jsfor test environment setup