rip-lang 3.14.4 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://github.com/shreeve/rip-lang/commits/main"><img src="https://img.shields.io/badge/version-3.14.4-blue.svg" alt="Version"></a>
12
+ <a href="https://github.com/shreeve/rip-lang/commits/main"><img src="https://img.shields.io/badge/version-3.15.0-blue.svg" alt="Version"></a>
13
13
  <a href="#zero-dependencies"><img src="https://img.shields.io/badge/dependencies-ZERO-brightgreen.svg" alt="Dependencies"></a>
14
14
  <a href="#"><img src="https://img.shields.io/badge/tests-1%2C436%2F1%2C436-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
@@ -66,8 +66,10 @@ Pre-built binaries and VS Code / Cursor extensions are published via GitHub Page
66
66
 
67
67
  **`ripdb`** — DuckDB extension that exposes a rip-db server as a first-class attached database:
68
68
 
69
+ ```bash
70
+ duckdb -unsigned # required for custom-repo extensions
71
+ ```
69
72
  ```sql
70
- SET allow_unsigned_extensions = true;
71
73
  INSTALL ripdb FROM 'https://shreeve.github.io/rip-lang/extensions/duckdb';
72
74
  LOAD ripdb;
73
75
  ```
@@ -349,7 +351,7 @@ The **body syntax is declarative**, not general Rip code. Five line forms are le
349
351
 
350
352
  **`:model` is where the pieces converge.** One declaration gives you a validator, a class with fields as enumerable own properties and methods/getters on the prototype, a chainable async query builder (`User.where(active: true).order("last_name").all!`), migration DDL that works standalone (`User.toSQL()` never touches the database), belongs-to/has-many accessors that resolve cross-module through the registry, and full shadow TypeScript with `ModelSchema<Instance, Data>` typing that propagates through schema algebra. Hydrated instances carry both snake_case and camelCase aliases on DB-derived columns (`order.user_id` and `order.userId` read the same slot), so raw SQL helpers and ORM access coexist cleanly. A single-function adapter interface (`adapter.query(sql, params)`) routes all database I/O, so tests use in-memory mocks and production uses rip-db without the ORM caring.
351
353
 
352
- **Schema algebra** — `.pick`, `.omit`, `.partial`, `.required`, `.extend` — always returns a new `:shape`. Field semantics (type, literal unions, constraints, inline transforms) carry through to the derived shape; instance behavior (methods, computed `~>`, eager-derived `!>`, hooks, and `@ensure` refinements) does not. `User.omit "password"` produces a validator for `User` minus the password field; it won't have `.find()` or the `beforeSave` hook, but field-level transforms (`email, -> it.email.toLowerCase()`) continue to fire on the derived shape exactly as they did on the original. This invariant is enforced both at runtime (ORM methods throw on derived shapes with a targeted diagnostic pointing at query projection) and at the TypeScript level (algebra generics are parameterized over `Data`, not `Instance`, so the derived types correctly omit methods and ORM surface). Internally, the whole feature is a compiler sidecar — 54% of the implementation lives in `src/schema.js` and touches the core compiler in under 100 lines of wiring. A four-layer lazy runtime (raw descriptor → normalized metadata → validator plan → ORM plan / DDL plan) means module load is cheap, migration scripts never build the ORM plan, and validator-only consumers never build the class machinery. The full reference is in [docs/RIP-SCHEMA.md](docs/RIP-SCHEMA.md).
354
+ **Schema algebra** — `.pick`, `.omit`, `.partial`, `.required`, `.extend` — always returns a new `:shape`. Field semantics (type, literal unions, constraints, inline transforms) carry through to the derived shape; instance behavior (methods, computed `~>`, eager-derived `!>`, hooks, and `@ensure` refinements) does not. `User.omit "password"` produces a validator for `User` minus the password field; it won't have `.find()` or the `beforeSave` hook, but field-level transforms (`email, -> it.email.toLowerCase()`) continue to fire on the derived shape exactly as they did on the original. This invariant is enforced both at runtime (ORM methods throw on derived shapes with a targeted diagnostic pointing at query projection) and at the TypeScript level (algebra generics are parameterized over `Data`, not `Instance`, so the derived types correctly omit methods and ORM surface). Internally, the whole feature is a compiler sidecar — 54% of the implementation lives in `packages/schema/src/schema.js` and touches the core compiler in under 100 lines of wiring. A four-layer lazy runtime (raw descriptor → normalized metadata → validator plan → ORM plan / DDL plan) means module load is cheap, migration scripts never build the ORM plan, and validator-only consumers never build the class machinery. The full reference is in [docs/RIP-SCHEMA.md](docs/RIP-SCHEMA.md).
353
355
 
354
356
  ---
355
357
 
@@ -365,9 +367,11 @@ Rip's reactivity is framework-agnostic — use it with React, Vue, Svelte, or va
365
367
 
366
368
  ---
367
369
 
368
- ## Rip UI
370
+ ## Rip App
371
+
372
+ Load `rip.min.js` (~88KB Brotli) — the Rip compiler and Rip App framework in one file. Components are `.rip` source files, compiled on demand, rendered with fine-grained reactivity. No build step. No bundler.
369
373
 
370
- Load `rip.min.js` (~54KB Brotli) — the Rip compiler and UI framework in one file. Components are `.rip` source files, compiled on demand, rendered with fine-grained reactivity. No build step. No bundler.
374
+ (Rip UI is the separate widget package at `packages/ui/`. The two terms do not overlap.)
371
375
 
372
376
  ```html
373
377
  <script defer src="rip.min.js" data-mount="Home"></script>
package/bin/rip CHANGED
@@ -5,6 +5,11 @@ import { execSync, spawnSync, spawn } from 'child_process';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { dirname, basename, join } from 'path';
7
7
  import { Compiler, formatError } from '../src/compiler.js';
8
+ // Side-effect imports — register CLI-side runtime providers. The browser
9
+ // bundle imports a different schema loader so server-only fragments
10
+ // (db-naming/orm/ddl) tree-shake out of docs/dist/rip.min.js.
11
+ import '../src/types-emit.js'; // registers emitTypes for .d.ts output
12
+ import '@rip-lang/schema/loader-server'; // registers full schema runtime
8
13
  import packageJson from '../package.json' with { type: 'json' };
9
14
 
10
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
package/docs/AGENTS.md CHANGED
@@ -37,7 +37,7 @@ App.mount()
37
37
  - `index.html` — playground
38
38
  - `demo.html` and `charts.html` — dashboard demos
39
39
  - `sierpinski.html` — CDN demo
40
- - `example/index.html` and `results/index.html` — app launchers / examples
40
+ - `example/index.html` and `results/index.html` — app launchers / examples. `example/index.json` is generated from `docs/demo/` via `bun run bundle:demo` (the source-of-truth lives in `docs/demo/`, the JSON is the deployable artifact).
41
41
  - `ui/index.html` — widget gallery
42
42
 
43
43
  Static demos can be opened via `file://`. The playground and example app require `bun run serve`.
package/docs/RIP-LANG.md CHANGED
@@ -105,8 +105,18 @@ colors = %w(red green blue) # any delimiter: [] () {} <> || !! etc.
105
105
 
106
106
  # Objects
107
107
  user = {name: "Alice", age: 30}
108
- shorthand = {name, age} # Same as {name: name, age: age}
109
- config = {api.host: "localhost", api.port: 3000} # Dotted keys → flat string keys
108
+ shorthand = {name, age} # Same as {name: name, age: age}
109
+
110
+ # Compound keys — dot- and hyphen-separated identifier chains collapse
111
+ # into a single flat string key. Useful for config trees and DOM-style
112
+ # attribute names without quote noise.
113
+ config = {api.host: "localhost", api.port: 3000} # → {'api.host': ..., 'api.port': ...}
114
+ attrs = {data-src: "/img.png", aria-label: "logo"} # → {'data-src': ..., 'aria-label': ...}
115
+ hosts = {beta-site.amazon.com: 100} # → {'beta-site.amazon.com': 100}
116
+
117
+ # No whitespace or newline is allowed on either side of `-` in a hyphen
118
+ # key — that's how `data-src` stays a single key while `a - b` stays
119
+ # subtraction. Dots are more permissive (whitespace either side is fine).
110
120
 
111
121
  # Map literals — real JavaScript Map with any key type
112
122
  table = *{
@@ -1460,7 +1470,7 @@ ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
1460
1470
  slug = read 'slug', 'slug' # URL-safe slug
1461
1471
  ```
1462
1472
 
1463
- ## Rip UIReactive Web Framework (built into rip-lang)
1473
+ ## Rip AppApplication Framework (built into rip-lang)
1464
1474
 
1465
1475
  Zero-build reactive framework. Ships the compiler to the browser and compiles `.rip` components on demand. File-based routing, unified reactive stash, and SSE hot reload.
1466
1476
 
@@ -2224,8 +2234,10 @@ a[/pat/, 1] # regex extract
2224
2234
  a? # existence check (a != null)
2225
2235
  a ?? b # nullish coalescing
2226
2236
 
2227
- # Dotted keys
2228
- {a.b: 1} # {'a.b': 1} — flat string key
2237
+ # Compound keys — dotted, hyphenated, or mixed (collapse to flat string keys)
2238
+ {a.b: 1} # {'a.b': 1}
2239
+ {data-src: 1} # {'data-src': 1} — no whitespace around `-`
2240
+ {beta-site.amazon.com: 100} # {'beta-site.amazon.com': 100} — mixed
2229
2241
 
2230
2242
  # Word arrays
2231
2243
  %w[foo bar baz] # ["foo", "bar", "baz"] — Ruby-style word literal
@@ -2324,7 +2324,7 @@ through this interface.
2324
2324
  ## 24. Compiler integration
2325
2325
 
2326
2326
  The schema keyword is implemented as a compiler sidecar in
2327
- `src/schema.js`, alongside the existing type and component sidecars.
2327
+ `packages/schema/src/schema.js`, alongside the existing type and component sidecars.
2328
2328
  This isolates the feature from the rest of the compiler: the main Rip
2329
2329
  grammar has two productions for the schema keyword (not hundreds), and
2330
2330
  the schema-specific body syntax never reaches the main parser.
@@ -2373,7 +2373,7 @@ binds to the instance correctly without special codegen.
2373
2373
 
2374
2374
  | File | Role |
2375
2375
  | -------------------- | ------------------------------------------------------------------ |
2376
- | `src/schema.js` | Sub-parser, `emitSchema`, Layer 1-4 runtime, shadow TS walker, `installSchemaSupport` |
2376
+ | `packages/schema/src/schema.js` | Sub-parser, `emitSchema`, Layer 1-4 runtime, shadow TS walker, `installSchemaSupport` |
2377
2377
  | `src/lexer.js` | Hook point — calls `rewriteSchema()`; comment-token fix for `#` modifier |
2378
2378
  | `src/grammar/grammar.rip` | The one `Schema` production |
2379
2379
  | `src/compiler.js` | Dispatch for the `schema` s-expression head; preamble injection |
@@ -2381,7 +2381,7 @@ binds to the instance correctly without special codegen.
2381
2381
  | `src/typecheck.js` | `hasSchemas()` probe so schema-only files aren't `@ts-nocheck`d |
2382
2382
  | `test/rip/schema.rip` | The test suite |
2383
2383
 
2384
- The total wiring in the core compiler (outside `src/schema.js`) is under
2384
+ The total wiring in the core compiler (outside `packages/schema/src/schema.js`) is under
2385
2385
  100 lines. That's the sidecar pattern working — the feature is big, but
2386
2386
  its footprint in the main compiler is small.
2387
2387
 
@@ -2405,7 +2405,7 @@ transactions, eager loading, scopes, and soft deletes — not yet; see
2405
2405
 
2406
2406
  **Does the runtime belong to `schema.js` or is it loaded separately?**
2407
2407
  It's inlined. When a file uses `schema`, the compiler injects a small
2408
- preamble (under `SCHEMA_RUNTIME` in `src/schema.js`) that defines
2408
+ preamble (under `SCHEMA_RUNTIME` in `packages/schema/src/schema.js`) that defines
2409
2409
  `SchemaError`, `__SchemaDef`, `__SchemaRegistry`, `Query`, and the
2410
2410
  helpers. No import statement, no package dependency, no bootstrap call.
2411
2411
 
@@ -0,0 +1,43 @@
1
+ # Rip App Demo
2
+
3
+ The canonical "everything in one file" demo of the Rip App framework. Six components covering layout, routing, reactive state, list rendering, and reusable component composition.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ docs/demo/
9
+ ├── components/
10
+ │ ├── _layout.rip — root layout with nav + error boundary
11
+ │ ├── index.rip — Home page (file-based routing: / → index.rip)
12
+ │ ├── counter.rip — reactive state, := / ~> persistence to stash
13
+ │ ├── todos.rip — list rendering, computed values, event handlers
14
+ │ ├── about.rip — uses the Card component for content sections
15
+ │ └── card.rip — reusable component with @children
16
+ └── css/
17
+ └── styles.css — design tokens + component styles, no Tailwind
18
+ ```
19
+
20
+ ## Bundle to one JSON file
21
+
22
+ ```bash
23
+ bun run bundle:demo
24
+ # wraps:
25
+ # bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t "Rip App Demo"
26
+ ```
27
+
28
+ Output: `docs/example/index.json` — a single ~17 KB file containing every component's raw `.rip` source plus all CSS, ready to ship to any static host. The launcher at `docs/example/index.html` fetches it once and runs the whole app from memory: no bundler, no build step, no per-component network requests.
29
+
30
+ ## Architecture
31
+
32
+ This is the "burn a CD" deployment model for a Rip app. The browser loads:
33
+
34
+ 1. `docs/dist/rip.min.js` (~80 KB Brotli) — the compiler + framework + reactive runtime
35
+ 2. `docs/example/index.json` (~17 KB) — the entire app's source
36
+
37
+ Then `<script type="text/rip">launch bundle: bundle</script>` mounts everything. Routing, reactivity, fine-grained DOM updates, all driven by code that compiles in the browser at mount time.
38
+
39
+ ## Iterating
40
+
41
+ Edit any `.rip` file under `components/`, then re-run `bun run bundle:demo` to refresh the bundled JSON. The launcher HTML at `docs/example/` will pick up the new bundle on next load.
42
+
43
+ CSS is concatenated alphabetically by filename. Add `.css` files under `css/` to extend the design.
@@ -0,0 +1,28 @@
1
+ # Root Layout (with error boundary and navigation indicator)
2
+
3
+ export Layout = component
4
+ errorMsg := null
5
+
6
+ ~> localStorage.setItem '__rip_route', @router.path
7
+
8
+ onError: (err) ->
9
+ errorMsg = err.message
10
+
11
+ render
12
+ .app-layout
13
+ nav.app-nav
14
+ .nav-inner
15
+ a.nav-brand "Rip App"
16
+ .nav-links
17
+ a.('nav-link', @router.path is '/' and 'active') href: "#/", "Home"
18
+ a.('nav-link', @router.path is '/counter' and 'active') href: "#counter", "Counter"
19
+ a.('nav-link', @router.path is '/todos' and 'active') href: "#todos", "Todos"
20
+ a.('nav-link', @router.path is '/about' and 'active') href: "#about", "About"
21
+ if @router.navigating
22
+ span.nav-loading "Loading..."
23
+ main.app-main
24
+ if errorMsg
25
+ .error-banner
26
+ p "Something went wrong: #{errorMsg}"
27
+ button.btn @click: (-> errorMsg = null), "Dismiss"
28
+ #content
@@ -0,0 +1,36 @@
1
+ # About Page — uses the Card component for content sections
2
+
3
+ export About = component
4
+
5
+ render
6
+ div
7
+ .page-header
8
+ h1.page-title "About Rip App"
9
+ p.page-subtitle "Zero-build reactive web apps!"
10
+
11
+ Card title: "The Idea"
12
+ p "Traditional frameworks build and bundle on the server, then ship static artifacts. Rip App ships the 40KB compiler to the browser instead. Components arrive as .rip source files, are compiled on demand, and render with fine-grained reactivity. No build step, no bundler, no configuration files."
13
+
14
+ Card title: "Two Keywords"
15
+ p "The component model adds exactly two keywords to the Rip language: component and render. Everything else — reactive state (:=), computed values (~=), effects (~>), methods, lifecycle hooks — is standard Rip syntax that already exists."
16
+
17
+ Card title: "Architecture"
18
+ p "Components live as source files in the unified stash. The Router maps URLs to components using file-based conventions (like Next.js). When you navigate, the Renderer compiles the matching .rip file and mounts the resulting component. Each reactive binding creates a direct DOM effect — no virtual DOM diffing."
19
+
20
+ Card title: "Stack"
21
+ .btn-group
22
+ .stat
23
+ span "Compiler "
24
+ span.stat-value "40KB"
25
+ .stat
26
+ span "Framework "
27
+ span.stat-value "~8KB"
28
+ .stat
29
+ span "Build step "
30
+ span.stat-value "None"
31
+ .stat
32
+ span "Dependencies "
33
+ span.stat-value "Zero"
34
+
35
+ Card title: "Modules"
36
+ p "The framework lives in one file: app.rip. It uses Rip's built-in reactive primitives directly — the same signals that power := and ~= in components. Deep reactive stash with path navigation. Component store for source management. File-based router mapping URLs to components. Renderer that compiles and mounts components with layout support."
@@ -0,0 +1,10 @@
1
+ # Card — reusable content card component
2
+
3
+ export Card = component
4
+ title =! ""
5
+
6
+ render
7
+ div.card
8
+ if title
9
+ h3 "#{title}"
10
+ @children
@@ -0,0 +1,33 @@
1
+ # Counter Page
2
+
3
+ export Counter = component
4
+ count := @app.data.count or 0
5
+ step := 1
6
+
7
+ ~> @app.data.count = count
8
+
9
+ increment: -> count += step
10
+ decrement: -> count -= step
11
+ reset: -> count = 0
12
+
13
+ render
14
+ div
15
+ .page-header
16
+ h1.page-title "Counter"
17
+ p.page-subtitle "Reactive state — persists across reload"
18
+
19
+ .card
20
+ .counter-display "#{count}"
21
+
22
+ .counter-controls
23
+ button.btn @click: @decrement, "- #{step}"
24
+ button.btn @click: @reset, "Reset"
25
+ button.btn.btn-primary @click: @increment, "+ #{step}"
26
+
27
+ .card
28
+ .card-title "Step Size"
29
+ .btn-group
30
+ button.btn @click: (-> step = 1), "1"
31
+ button.btn @click: (-> step = 5), "5"
32
+ button.btn @click: (-> step = 10), "10"
33
+ button.btn @click: (-> step = 100), "100"
@@ -0,0 +1,30 @@
1
+ # Home Page
2
+
3
+ export Home = component
4
+
5
+ render
6
+ div
7
+ .page-header
8
+ h1.page-title "Rip App Framework"
9
+ p.page-subtitle "Zero-build reactive web apps"
10
+
11
+ .feature-grid
12
+ .feature-card
13
+ .feature-icon "🗂"
14
+ .feature-name "Component Store"
15
+ .feature-desc "Component source files loaded on demand. Compiled in the browser."
16
+
17
+ .feature-card
18
+ .feature-icon "🔀"
19
+ .feature-name "File-Based Router"
20
+ .feature-desc "URLs map to components. Dynamic segments and nested layouts."
21
+
22
+ .feature-card
23
+ .feature-icon "⚡"
24
+ .feature-name "Reactive Stash"
25
+ .feature-desc "Deep state tree with path navigation. Every property tracked."
26
+
27
+ .feature-card
28
+ .feature-icon "🎯"
29
+ .feature-name "Fine-Grained Rendering"
30
+ .feature-desc "No virtual DOM. Direct DOM updates via reactive effects."
@@ -0,0 +1,48 @@
1
+ # Todos Page
2
+
3
+ export Todos = component
4
+ newTodo := ''
5
+ todos := @app.data.todos or []
6
+ nextId := (@app.data.nextId or 1)
7
+
8
+ ~> @app.data.todos = todos
9
+ ~> @app.data.nextId = nextId
10
+
11
+ remaining ~= todos.filter((t) -> not t.done).length
12
+
13
+ addTodo: ->
14
+ if newTodo.trim()
15
+ todos = [...todos, { id: nextId, text: newTodo.trim(), done: false }]
16
+ nextId++
17
+ newTodo = ''
18
+
19
+ deleteTodo: (id) ->
20
+ todos = todos.filter (t) -> t.id isnt id
21
+
22
+ clearAll: ->
23
+ todos = []
24
+
25
+ handleKey: (e) ->
26
+ if e.key is 'Enter'
27
+ @addTodo()
28
+
29
+ render
30
+ div
31
+ .page-header
32
+ h1.page-title "Todos"
33
+ p.page-subtitle "List rendering and state management"
34
+
35
+ .card
36
+ .todo-input-row
37
+ input.input type: "text", value <=> newTodo, placeholder: "What needs to be done?", @keydown: @handleKey
38
+ button.btn.btn-primary @click: @addTodo, "Add"
39
+
40
+ ul.todo-list
41
+ for todo in todos
42
+ li.todo-item
43
+ span.todo-text todo.text
44
+ button.todo-delete @click: (=> @deleteTodo(todo.id)), "x"
45
+
46
+ .todo-stats
47
+ span "#{remaining} remaining"
48
+ button.btn.btn-sm @click: @clearAll, "Clear all"