rip-lang 3.17.4 → 3.17.5

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.
Binary file
package/docs/ui/index.css CHANGED
@@ -1281,6 +1281,14 @@ body:has(.source-overlay) { overflow: hidden; }
1281
1281
  border: none; outline: none; padding: 0 12px; height: 36px;
1282
1282
  font: inherit; font-size: 14px; flex: 1; min-width: 0;
1283
1283
  }
1284
+
1285
+ /* Round the end children to the inner radius (12px border-radius − 1px
1286
+ border) so an addon fills flush into the corner. Without this, the
1287
+ square-cornered addon is clipped by overflow:hidden to a slightly
1288
+ different curve than the border, leaving a faint sliver at the corners
1289
+ (most visible on the gray prefix/suffix when focused). */
1290
+ & > :first-child { border-top-left-radius: 11px; border-bottom-left-radius: 11px; }
1291
+ & > :last-child { border-top-right-radius: 11px; border-bottom-right-radius: 11px; }
1284
1292
  }
1285
1293
 
1286
1294
  /* ── ButtonGroup ── */
@@ -9,7 +9,7 @@
9
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
10
  <link rel="stylesheet" href="index.css">
11
11
  <style>body { opacity: 0; } body.ready { opacity: 1; transition: opacity 200ms ease-in; }</style>
12
- <script defer src="../dist/rip.min.js" data-src="bundle.json" data-mount="WidgetGallery">
12
+ <script defer src="../dist/rip.min.js" data-src="/app" data-mount="WidgetGallery">
13
13
  </script>
14
14
  </head>
15
15
  <body>
@@ -103,9 +103,9 @@ export WidgetGallery = component
103
103
  return unless entry
104
104
  sourceName = entry.name
105
105
  sourceLines = entry.lines
106
- src = window.__RIP__?.components?.read("_pkg/ui/#{id}.rip")
107
- return unless src
108
- sourceCode = src
106
+ resp = fetch! "browser/components/#{id}.rip"
107
+ return unless resp.ok
108
+ sourceCode = resp.text!
109
109
  _closeSource: -> sourceCode = null
110
110
  _stopProp: (e) -> e.stopPropagation()
111
111
  _ensureHljs: (cb) ->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.17.4",
3
+ "version": "3.17.5",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
@@ -9,7 +9,7 @@
9
9
  "packages/*"
10
10
  ],
11
11
  "catalog": {
12
- "typescript": "5.9.3"
12
+ "typescript": "6.0.3"
13
13
  },
14
14
  "browser": "docs/dist/rip.min.js",
15
15
  "exports": {
@@ -38,7 +38,6 @@
38
38
  "build": "bun scripts/build.js",
39
39
  "bump": "bun scripts/bump.js",
40
40
  "gen:dom": "bun scripts/gen-dom.js",
41
- "gallery": "bun scripts/gallery.js",
42
41
  "bundle:demo": "bun scripts/bundle-app.js docs/demo/routes --prefix _route --css docs/demo/css -o docs/example/index.json -t 'Rip App Demo'",
43
42
  "bundle:ui": "bun scripts/bundle-app.js packages/ui/browser/components --prefix _pkg/ui -o docs/ui/bundle.json -t 'Rip UI'",
44
43
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
@@ -49,6 +48,7 @@
49
48
  "serve": "bun scripts/serve.js",
50
49
  "test": "bun test/runner.js",
51
50
  "test:types": "bun test/types/runner.js",
51
+ "test:check": "bun test/check.test.js",
52
52
  "test:sourcemap-corpus": "bun test/types/sourcemap-corpus.js",
53
53
  "test:diagnostic-corpus": "bun test/types/diagnostic-corpus.js",
54
54
  "test:markers": "bun test/markers.test.js",
@@ -66,7 +66,7 @@
66
66
  "test:server": "./bin/rip packages/server/tests/runner.rip",
67
67
  "test:time": "bun run --cwd packages/time test",
68
68
  "test:validate": "./bin/rip packages/validate/test.rip",
69
- "test:all": "bun run test && bun run test:types && bun run test:sourcemap-corpus && bun run test:diagnostic-corpus && bun run test:markers && bun run test:error-span && bun run test:schema && bun run test:schema-fresh && bun run test:docs && bun run test:loader && bun run test:graph && bun run test:bundle && bun run test:grammar && bun run test:parser-fresh && bun run test:semantic && bun run test:server && bun run test:time && bun run test:validate",
69
+ "test:all": "bun run test && bun run test:types && bun run test:check && bun run test:sourcemap-corpus && bun run test:diagnostic-corpus && bun run test:markers && bun run test:error-span && bun run test:schema && bun run test:schema-fresh && bun run test:docs && bun run test:loader && bun run test:graph && bun run test:bundle && bun run test:grammar && bun run test:parser-fresh && bun run test:semantic && bun run test:server && bun run test:time && bun run test:validate",
70
70
  "test:ui": "bun run --cwd packages/ui test:e2e",
71
71
  "test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
72
72
  "test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
@@ -102,7 +102,7 @@
102
102
  "license": "MIT",
103
103
  "dependencies": {
104
104
  "@types/bun": "1.3.14",
105
- "typescript": "5.9.3"
105
+ "typescript": "6.0.3"
106
106
  },
107
107
  "devDependencies": {
108
108
  "vscode-oniguruma": "2.0.1",
package/src/AGENTS.md CHANGED
@@ -33,7 +33,7 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
33
33
  | `src/error.js` | yes | runtime error formatting |
34
34
  | `src/sourcemaps.js` | yes | inline source-map generation |
35
35
  | `src/generated/dom-tags.js` | yes | HTML/SVG tag set for render-block tag detection |
36
- | `src/generated/dom-events.js` | yes | event-name set for `onClick`/`onKeydown` auto-wire |
36
+ | `src/generated/dom-events.js` | yes | DOM event-name set the bare `@event` shorthand validates against |
37
37
  | `src/dts.js` | **no** | `.d.ts` emitter + intrinsic decl tables for the type system — CLI / typecheck only |
38
38
  | `src/schema/dts.js` | **no** | `.d.ts` emitter for schema declarations — CLI / typecheck only |
39
39
  | `src/typecheck.js` | **no** | TypeScript LSP integration — CLI only |
@@ -433,6 +433,22 @@ Key entry points:
433
433
  - `emitTemplateLoop` — emits `__reconcile(...)`
434
434
  - `emitBlockFactory` — shared factory emitter used by conditionals and loops
435
435
 
436
+ ### Member Declaration Semantics (uniform `=` / `:=` / `=!`)
437
+
438
+ The declaration operator decides a member's category in `emitComponent`'s categorizer (the `for (let stmt of statements)` loop). The buckets:
439
+
440
+ - `:=` → `stateVars` → `__state(...)`; added to `reactiveMembers` **and** `refTargetMembers`.
441
+ - `=` (non-function, non-lifecycle) → `plainVars` → a **plain property** `this.name = value` (public: `this.name = props.name ?? value`). **Not** added to `reactiveMembers` and **not** to `refTargetMembers`. This is uniform with `=` everywhere else in the language — a `=` member is a plain, reassignable field, not reactive.
442
+ - `=!` → `readonlyVars` → plain const field.
443
+ - `~=` → `derivedVars` (`__computed`); `~>` → `effects`; a `->`/`=>` value or `name: -> …` → `methods`.
444
+ - bare `@name` / `@name :=` → public `stateVars` (reactive props, share the parent signal via `__bind_name__`).
445
+
446
+ Because a plain member is in `componentMembers` but **not** `reactiveMembers`, `transformComponentMembers` rewrites its reads/writes to plain `this.name` (reactive members become `this.name.value`). One subtlety lives in the `block`/`program` branch of `transformComponentMembers`: a bare `name = value` introduces a *local* only when `name` is **not** a `componentMembers` entry — otherwise a write to a plain member would shadow it with a `let`. (Previously this checked `reactiveMembers`, which was equivalent only because every non-method `=` member used to be reactive.)
447
+
448
+ Both emit paths handle `plainVars`: the runtime class (`_init`) and the type-check stub (`stubComponents` — a `declare name: T` property plus an `_init` assignment, mirroring `readonlyVars`). `dts.js` leaves private `=` members invisible in emitted `.d.ts` (unchanged — they were already not surfaced).
449
+
450
+ **Silent-freeze diagnostic.** After categorization, `emitComponent` errors when a **private** `plainVars` member is *read* in `render` / a `~=` computed / a `~>` effect **and** *reassigned* anywhere in the component (methods, effects, lifecycle hooks, computed bodies, render event handlers). That pair is the silent-freeze class: a non-reactive field read reactively but mutated never re-renders. The message steers to `:=`. Public `@name = v` fields are exempt (explicit rare opt-in).
451
+
436
452
  ### Factory Mode
437
453
 
438
454
  Block factories need locals and `ctx.member` references instead of `this._elN` and `this.member`.
@@ -446,15 +462,30 @@ Block factories need locals and `ctx.member` references instead of `this._elN` a
446
462
 
447
463
  Factory mode is entered in `emitConditionBranch` and `emitTemplateLoop` via save/restore of `[_createLines, _setupLines, _factoryMode, _factoryVars]`.
448
464
 
449
- ### Auto-Wired Event Handlers
450
-
451
- Methods named `onClick`, `onKeydown`, `onMouseenter`, etc. automatically bind to the root DOM element.
452
-
453
- - detection: methods matching `/^on[A-Z]/` that are not lifecycle hooks
454
- - event names come from the generated `src/generated/dom-events.js` list, sourced from TypeScript's `HTMLElementEventMap`
455
- - root only: the first generated HTML tag can claim auto-wiring
456
- - explicit override: `@click: handler` on the root suppresses auto-wire for that event
457
- - lifecycle exclusion: `onError` is not auto-wired
465
+ ### Event Directives
466
+
467
+ Events bind where they're declared in `render`, never by method name. A bare
468
+ `@click` is shorthand for `@click: @onClick` — a `this`-member handler that
469
+ resolves only to a component method (never a local) — bound to the element it
470
+ sits on. Defining `onClick` alone attaches nothing.
471
+
472
+ - positions: a tag-head argument (`button @click`, `button x: 1, @click`) or an
473
+ element-body statement line (`@click` on its own line under the element). Both
474
+ desugar in `rewriteRender` (`isBareEventAttr` + `elementBodyLevels`); a `:`
475
+ before it (`value: @click`) or being nested in `()`/`[]`/`{}` excludes it.
476
+ - name resolution: `@<event>` → `on<Event>`, capitalizing only the first letter
477
+ (`@keydown` → `onKeydown`, `@pointerdown` → `onPointerdown`) — not React-style
478
+ - validation (in the emitter choke point `_checkBareEventHandler`): the name must
479
+ be in the generated `src/generated/dom-events.js` set, else a compile error
480
+ steering to `= @name`; bare `@error` is rejected (clashes with the `onError`
481
+ lifecycle hook — bind a DOM error listener with `@error: handler`); a missing
482
+ `on<Event>` method is a compile error
483
+ - a `.`-chain (`@toast.title`) is a member path, never a bare event
484
+ - explicit `@event: handler` takes any expression (locals, `obj.method`, inline
485
+ arrows) and works in any attribute position; `@event.modifier: handler` wraps
486
+ to a computed key
487
+ - a bare `@member` standing as a render child (not a directive) is a compile
488
+ error — `= @member` / `"#{@member}"` render text, `slot` projects children
458
489
  - after bumping `typescript`, refresh the generated DOM metadata with `bun run gen:dom`
459
490
 
460
491
  ### Inherited Props (`extends <tag>`)
@@ -479,6 +510,8 @@ export Checkbox = component
479
510
  @onClick() if e.key in ['Enter', ' ']
480
511
  render
481
512
  button role: 'checkbox', aria-checked: !!@checked
513
+ @click
514
+ @keydown
482
515
  slot
483
516
  ```
484
517
 
package/src/compiler.js CHANGED
@@ -282,6 +282,9 @@ export class CodeEmitter {
282
282
 
283
283
  // Symbol literals
284
284
  'symbol': 'emitSymbol',
285
+
286
+ // `expr as Type` cast — runtime-erased, narrows in the check path
287
+ 'cast': 'emitCast',
285
288
  };
286
289
 
287
290
  constructor(options = {}) {
@@ -932,8 +935,28 @@ export class CodeEmitter {
932
935
  // Main dispatch
933
936
  // ---------------------------------------------------------------------------
934
937
 
938
+ // `expr as Type` cast wrap. At runtime the cast is fully erased, so this only
939
+ // fires in the shadow-TS check path (`inlineTypes`), where it re-materializes
940
+ // a real TS assertion `(code as Type)`. `typeStr` is the opaque type string
941
+ // collapsed by the type rewriter onto the CAST marker token.
942
+ castWrap(code, typeStr) {
943
+ if (!typeStr || !this.options.inlineTypes) return code;
944
+ return `(${code} as ${typeStr})`;
945
+ }
946
+
947
+ // `['cast', expr, typeStr]` — postfix type assertion. Emit just the inner
948
+ // expression on the runtime path (the cast leaves zero trace); in the check
949
+ // path wrap it as a real TS assertion so it narrows for the checker. Covers
950
+ // every carrier (identifier, member, call/index/paren result) because the
951
+ // grammar reduces this AFTER the full postfix expression is built. Chains
952
+ // nest (`['cast',['cast',x,'A'],'B']`) and re-wrap recursively.
953
+ emitCast(head, rest) {
954
+ let [expr, typeStr] = rest;
955
+ return this.castWrap(this.emit(expr, 'value'), typeStr);
956
+ }
957
+
935
958
  emit(sexpr, context = 'statement') {
936
- // String object with metadata (quote, bang, optional, heregex, etc.)
959
+ // String object with metadata (quote, bang, optional, heregex, cast, etc.)
937
960
  if (sexpr instanceof String) {
938
961
  // Dammit operator (!)
939
962
  if (meta(sexpr, 'bang') === true) {
@@ -1174,9 +1197,9 @@ export class CodeEmitter {
1174
1197
 
1175
1198
  if (this.usesReactivity && !skip) {
1176
1199
  if (skipRT) {
1177
- code += 'var { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
1200
+ code += 'var { __state, __computed, __effect, __batch, __detachRef, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
1178
1201
  } else if (typeof globalThis !== 'undefined' && globalThis.__rip) {
1179
- code += 'const { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
1202
+ code += 'const { __state, __computed, __effect, __batch, __detachRef, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
1180
1203
  } else {
1181
1204
  code += this.getReactiveRuntime();
1182
1205
  }
@@ -4756,6 +4779,17 @@ function __batch(fn) {
4756
4779
  }
4757
4780
  }
4758
4781
 
4782
+ // Compare-and-clear a template ref cell on element detach. Uses the
4783
+ // NON-tracking read() so clearing never subscribes the teardown path to
4784
+ // the cell, and only nulls the cell if it still holds THIS element — a
4785
+ // keyed move or a re-render that already pointed the cell elsewhere must
4786
+ // not be clobbered. Writing null notifies subscribers (an effect reading
4787
+ // the ref re-runs when the element disappears); callers wrap teardown in
4788
+ // __batch where parent subscribers may be live.
4789
+ function __detachRef(cell, el) {
4790
+ if (cell && typeof cell.read === 'function' && cell.read() === el) cell.value = null;
4791
+ }
4792
+
4759
4793
  // Returns the AbortSignal of the currently-running effect, or null if
4760
4794
  // called outside an effect or before AbortController is available.
4761
4795
  // Designed for async-aware effect bodies — capture the signal BEFORE
@@ -4815,7 +4849,7 @@ function __catchErrors(fn) {
4815
4849
 
4816
4850
  // Register on globalThis for runtime deduplication
4817
4851
  if (typeof globalThis !== 'undefined') {
4818
- globalThis.__rip = { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors, __getEffectSignal };
4852
+ globalThis.__rip = { __state, __computed, __effect, __batch, __detachRef, __readonly, __setErrorHandler, __handleError, __catchErrors, __getEffectSignal };
4819
4853
  // Stdlib-style global so user code can call getEffectSignal() in a
4820
4854
  // ~> body without importing or destructuring. Mirrors how p, pp,
4821
4855
  // assert, etc. are registered for ergonomic use.