rip-lang 3.7.3 → 3.7.4

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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ All notable changes to Rip will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
+ ## [3.7.4] - 2026-02-11
11
+
12
+ ### Compiler — Nested Function Scope Chain
13
+
14
+ - **Scope stack for variable hoisting** — Nested functions no longer re-declare variables from enclosing function scopes. Previously, the compiler only checked program-level variables; now it tracks a full scope chain. Fixes incorrect `let` shadowing in deeply nested function patterns (e.g., `mountRoute` inside `createRenderer` in `ui.rip`).
15
+
16
+ ### Rip UI — Demo App Working
17
+
18
+ - **Router `navigating` getter/setter** — The `_navigating` signal is now accessible through `router.navigating` (read/write), fixing a cross-scope reference where `createRenderer` accessed a variable local to `createRouter`.
19
+ - **Component props passthrough** — `__Component` base class now assigns all props to `this` via `Object.assign(this, props)` before `_init`, so components can access `@router`, `@app`, `@params` etc.
20
+
21
+ ### Language — `*@` Merge-This
22
+
23
+ - **`*@ = props`** — New merge-assign variant for `this`. Compiles to `Object.assign(this, props)`. Natural extension of `*obj = expr` — no `??=` guard needed since `this` is never null.
24
+
10
25
  ## [3.7.3] - 2026-02-11
11
26
 
12
27
  ### Fixes & Polish
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.7.3-blue.svg" alt="Version"></a>
12
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/version-3.7.4-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-1225%2F1225-brightgreen.svg" alt="Tests"></a>
15
15
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License"></a>
@@ -267,7 +267,7 @@ Counter = component
267
267
 
268
268
  Two keywords — `component` and `render` — are all the language adds. Everything else (`:=` state, `~=` computed, methods, lifecycle) is standard Rip.
269
269
 
270
- See [@rip-lang/ui](packages/ui/) for the full framework: file-based router, reactive stash, parts system, and component renderer.
270
+ See [@rip-lang/ui](packages/ui/) for the full framework: file-based router, reactive stash, component store, and renderer.
271
271
 
272
272
  ---
273
273
 
@@ -344,7 +344,7 @@ Rip includes optional packages for full-stack development:
344
344
 
345
345
  | Package | Version | Purpose |
346
346
  |---------|---------|---------|
347
- | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.7.3 | Core language compiler |
347
+ | [rip-lang](https://www.npmjs.com/package/rip-lang) | 3.7.4 | Core language compiler |
348
348
  | [@rip-lang/api](packages/api/) | 1.1.4 | HTTP framework (Sinatra-style routing, 37 validators) |
349
349
  | [@rip-lang/server](packages/server/) | 1.1.3 | Multi-worker app server (hot reload, HTTPS, mDNS) |
350
350
  | [@rip-lang/db](packages/db/) | 1.1.2 | DuckDB server with official UI (pure Bun FFI) |
@@ -2049,8 +2049,19 @@ class Lexer {
2049
2049
  tag = "REACT_ASSIGN";
2050
2050
  else if (val === "=!")
2051
2051
  tag = "READONLY_ASSIGN";
2052
- else if (val === "*" && (!prev || prev[0] === "TERMINATOR" || prev[0] === "INDENT" || prev[0] === "OUTDENT") && /^[a-zA-Z_$]/.test(this.chunk[1] || "")) {
2052
+ else if (val === "*" && (!prev || prev[0] === "TERMINATOR" || prev[0] === "INDENT" || prev[0] === "OUTDENT") && (/^[a-zA-Z_$]/.test(this.chunk[1] || "") || this.chunk[1] === "@")) {
2053
2053
  let rest = this.chunk.slice(1);
2054
+ let mAt = /^@(\s*)=(?!=)/.exec(rest);
2055
+ if (mAt) {
2056
+ let space = mAt[1];
2057
+ this.emit("IDENTIFIER", "Object");
2058
+ this.emit(".", ".");
2059
+ let t = this.emit("PROPERTY", "assign");
2060
+ t.spaced = true;
2061
+ this.emit("@", "@");
2062
+ this.emit(",", ",");
2063
+ return 1 + 1 + space.length + 1;
2064
+ }
2054
2065
  let m = /^((?:(?!\s)[$\w\x7f-\uffff])+(?:\.[a-zA-Z_$][\w]*)*)(\s*)=(?!=)/.exec(rest);
2055
2066
  if (m) {
2056
2067
  let target = m[1], space = m[2];
@@ -2366,7 +2377,7 @@ class Lexer {
2366
2377
  let prevToken = i > 0 ? tokens[i - 1] : null;
2367
2378
  let prevTag = prevToken ? prevToken[0] : null;
2368
2379
  let atLineStart = prevTag === "INDENT" || prevTag === "TERMINATOR";
2369
- let cxToken = gen("PROPERTY", "__cx__", token);
2380
+ let cxToken = gen("PROPERTY", "__clsx", token);
2370
2381
  nextToken[0] = "CALL_START";
2371
2382
  let depth = 1;
2372
2383
  for (let j = i + 2;j < tokens.length && depth > 0; j++) {
@@ -2416,6 +2427,10 @@ class Lexer {
2416
2427
  return 3;
2417
2428
  }
2418
2429
  }
2430
+ if (tag === "IDENTIFIER" && isComponent(token[1]) && nextToken && (nextToken[0] === "OUTDENT" || nextToken[0] === "TERMINATOR")) {
2431
+ tokens.splice(i + 1, 0, gen("CALL_START", "(", token), gen("CALL_END", ")", token));
2432
+ return 3;
2433
+ }
2419
2434
  return 1;
2420
2435
  });
2421
2436
  }
@@ -3579,6 +3594,9 @@ function installComponentSupport(CodeGenerator) {
3579
3594
  if (typeof sexpr === "string" && this.reactiveMembers && this.reactiveMembers.has(sexpr)) {
3580
3595
  return [".", [".", "this", sexpr], "value"];
3581
3596
  }
3597
+ if (typeof sexpr === "string" && this.componentMembers && this.componentMembers.has(sexpr)) {
3598
+ return [".", "this", sexpr];
3599
+ }
3582
3600
  return sexpr;
3583
3601
  }
3584
3602
  if (sexpr[0] === "." && sexpr[1] === "this" && typeof sexpr[2] === "string") {
@@ -3673,17 +3691,15 @@ function installComponentSupport(CodeGenerator) {
3673
3691
  this.reactiveMembers = reactiveMembers;
3674
3692
  const lines = [];
3675
3693
  let blockFactoriesCode = "";
3676
- lines.push("class {");
3677
- lines.push(" constructor(props = {}) {");
3678
- lines.push(" const __prevComponent = __pushComponent(this);");
3679
- lines.push("");
3694
+ lines.push("class extends __Component {");
3695
+ lines.push(" _init(props) {");
3680
3696
  for (const { name, value } of readonlyVars) {
3681
3697
  const val = this.generateInComponent(value, "value");
3682
3698
  lines.push(` this.${name} = props.${name} ?? ${val};`);
3683
3699
  }
3684
3700
  for (const { name, value } of stateVars) {
3685
3701
  const val = this.generateInComponent(value, "value");
3686
- lines.push(` this.${name} = isSignal(props.${name}) ? props.${name} : __state(props.${name} ?? ${val});`);
3702
+ lines.push(` this.${name} = __state(props.${name} ?? ${val});`);
3687
3703
  }
3688
3704
  for (const { name, expr } of derivedVars) {
3689
3705
  const val = this.generateInComponent(expr, "value");
@@ -3694,8 +3710,6 @@ function installComponentSupport(CodeGenerator) {
3694
3710
  const effectCode = this.generateInComponent(effectBody, "value");
3695
3711
  lines.push(` __effect(${effectCode});`);
3696
3712
  }
3697
- lines.push("");
3698
- lines.push(" __popComponent(__prevComponent);");
3699
3713
  lines.push(" }");
3700
3714
  for (const { name, func } of methods) {
3701
3715
  if (Array.isArray(func) && (func[0] === "->" || func[0] === "=>")) {
@@ -3736,21 +3750,6 @@ function installComponentSupport(CodeGenerator) {
3736
3750
  lines.push(" }");
3737
3751
  }
3738
3752
  }
3739
- lines.push(" mount(target) {");
3740
- lines.push(' if (typeof target === "string") target = document.querySelector(target);');
3741
- lines.push(" this._target = target;");
3742
- lines.push(" this._root = this._create();");
3743
- lines.push(" target.appendChild(this._root);");
3744
- lines.push(" if (this._setup) this._setup();");
3745
- lines.push(" if (this.mounted) this.mounted();");
3746
- lines.push(" return this;");
3747
- lines.push(" }");
3748
- lines.push(" unmount() {");
3749
- lines.push(" if (this.unmounted) this.unmounted();");
3750
- lines.push(" if (this._root && this._root.parentNode) {");
3751
- lines.push(" this._root.parentNode.removeChild(this._root);");
3752
- lines.push(" }");
3753
- lines.push(" }");
3754
3753
  lines.push("}");
3755
3754
  this.componentMembers = prevComponentMembers;
3756
3755
  this.reactiveMembers = prevReactiveMembers;
@@ -3767,6 +3766,9 @@ ${blockFactoriesCode}return ${lines.join(`
3767
3766
  if (typeof sexpr === "string" && this.reactiveMembers && this.reactiveMembers.has(sexpr)) {
3768
3767
  return `this.${sexpr}.value`;
3769
3768
  }
3769
+ if (typeof sexpr === "string" && this.componentMembers && this.componentMembers.has(sexpr)) {
3770
+ return `this.${sexpr}`;
3771
+ }
3770
3772
  if (Array.isArray(sexpr) && this.reactiveMembers) {
3771
3773
  const transformed = this.transformComponentMembers(sexpr);
3772
3774
  return this.generate(transformed, context);
@@ -3853,11 +3855,9 @@ ${blockFactoriesCode}return ${lines.join(`
3853
3855
  this._setupLines.push(`__effect(() => { ${textVar3}.data = this.${prop}.value; });`);
3854
3856
  return textVar3;
3855
3857
  }
3856
- if (this.componentMembers && this.componentMembers.has(prop)) {
3857
- const slotVar = this.newElementVar("slot");
3858
- this._createLines.push(`${slotVar} = this.${prop} instanceof Node ? this.${prop} : (this.${prop} != null ? document.createTextNode(String(this.${prop})) : document.createComment(''));`);
3859
- return slotVar;
3860
- }
3858
+ const slotVar = this.newElementVar("slot");
3859
+ this._createLines.push(`${slotVar} = this.${prop} instanceof Node ? this.${prop} : (this.${prop} != null ? document.createTextNode(String(this.${prop})) : document.createComment(''));`);
3860
+ return slotVar;
3861
3861
  }
3862
3862
  const { tag, classes } = this.collectTemplateClasses(sexpr);
3863
3863
  if (tag && this.isHtmlTag(tag)) {
@@ -3869,14 +3869,14 @@ ${blockFactoriesCode}return ${lines.join(`
3869
3869
  return textVar2;
3870
3870
  }
3871
3871
  if (Array.isArray(head)) {
3872
- if (Array.isArray(head[0]) && head[0][0] === "." && (head[0][2] === "__cx__" || head[0][2] instanceof String && head[0][2].valueOf() === "__cx__")) {
3872
+ if (Array.isArray(head[0]) && head[0][0] === "." && (head[0][2] === "__clsx" || head[0][2] instanceof String && head[0][2].valueOf() === "__clsx")) {
3873
3873
  const tag2 = typeof head[0][1] === "string" ? head[0][1] : head[0][1].valueOf();
3874
3874
  const classExprs = head.slice(1);
3875
3875
  return this.generateDynamicTag(tag2, classExprs, rest);
3876
3876
  }
3877
3877
  const { tag, classes } = this.collectTemplateClasses(head);
3878
3878
  if (tag && this.isHtmlTag(tag)) {
3879
- if (classes.length === 1 && classes[0] === "__cx__") {
3879
+ if (classes.length === 1 && classes[0] === "__clsx") {
3880
3880
  return this.generateDynamicTag(tag, rest, []);
3881
3881
  }
3882
3882
  return this.generateTag(tag, classes, rest);
@@ -3960,9 +3960,9 @@ ${blockFactoriesCode}return ${lines.join(`
3960
3960
  const classArgs = classExprs.map((e) => this.generateInComponent(e, "value")).join(", ");
3961
3961
  const hasReactive = classExprs.some((e) => this.hasReactiveDeps(e));
3962
3962
  if (hasReactive) {
3963
- this._setupLines.push(`__effect(() => { ${elVar}.className = __cx__(${classArgs}); });`);
3963
+ this._setupLines.push(`__effect(() => { ${elVar}.className = __clsx(${classArgs}); });`);
3964
3964
  } else {
3965
- this._createLines.push(`${elVar}.className = __cx__(${classArgs});`);
3965
+ this._createLines.push(`${elVar}.className = __clsx(${classArgs});`);
3966
3966
  }
3967
3967
  }
3968
3968
  for (const arg of children) {
@@ -4006,8 +4006,12 @@ ${blockFactoriesCode}return ${lines.join(`
4006
4006
  let [key, value] = objExpr[i];
4007
4007
  if (this.is(key, ".") && key[1] === "this") {
4008
4008
  const eventName = key[2];
4009
- const handlerCode = this.generateInComponent(value, "value");
4010
- this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => (${handlerCode})(e));`);
4009
+ if (typeof value === "string" && this.componentMembers?.has(value)) {
4010
+ this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => this.${value}(e));`);
4011
+ } else {
4012
+ const handlerCode = this.generateInComponent(value, "value");
4013
+ this._createLines.push(`${elVar}.addEventListener('${eventName}', (e) => (${handlerCode})(e));`);
4014
+ }
4011
4015
  continue;
4012
4016
  }
4013
4017
  if (typeof key === "string") {
@@ -4363,10 +4367,6 @@ ${blockFactoriesCode}return ${lines.join(`
4363
4367
  // Rip Component Runtime
4364
4368
  // ============================================================================
4365
4369
 
4366
- function isSignal(v) {
4367
- return v != null && typeof v === 'object' && typeof v.read === 'function';
4368
- }
4369
-
4370
4370
  let __currentComponent = null;
4371
4371
 
4372
4372
  function __pushComponent(component) {
@@ -4404,13 +4404,38 @@ function hasContext(key) {
4404
4404
  return false;
4405
4405
  }
4406
4406
 
4407
- function __cx__(...args) {
4407
+ function __clsx(...args) {
4408
4408
  return args.filter(Boolean).join(' ');
4409
4409
  }
4410
4410
 
4411
+ class __Component {
4412
+ constructor(props = {}) {
4413
+ Object.assign(this, props);
4414
+ const prev = __pushComponent(this);
4415
+ this._init(props);
4416
+ __popComponent(prev);
4417
+ }
4418
+ _init() {}
4419
+ mount(target) {
4420
+ if (typeof target === "string") target = document.querySelector(target);
4421
+ this._target = target;
4422
+ this._root = this._create();
4423
+ target.appendChild(this._root);
4424
+ if (this._setup) this._setup();
4425
+ if (this.mounted) this.mounted();
4426
+ return this;
4427
+ }
4428
+ unmount() {
4429
+ if (this.unmounted) this.unmounted();
4430
+ if (this._root && this._root.parentNode) {
4431
+ this._root.parentNode.removeChild(this._root);
4432
+ }
4433
+ }
4434
+ }
4435
+
4411
4436
  // Register on globalThis for runtime deduplication
4412
4437
  if (typeof globalThis !== 'undefined') {
4413
- globalThis.__ripComponent = { isSignal, __pushComponent, __popComponent, setContext, getContext, hasContext, __cx__ };
4438
+ globalThis.__ripComponent = { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __Component };
4414
4439
  }
4415
4440
 
4416
4441
  `;
@@ -4797,6 +4822,7 @@ class CodeGenerator {
4797
4822
  this.programVars = new Set;
4798
4823
  this.functionVars = new Map;
4799
4824
  this.helpers = new Set;
4825
+ this.scopeStack = [];
4800
4826
  this.collectProgramVariables(sexpr);
4801
4827
  let code = this.generate(sexpr);
4802
4828
  if (this.sourceMap)
@@ -5226,7 +5252,7 @@ class CodeGenerator {
5226
5252
  }
5227
5253
  if (this.usesTemplates && !skip) {
5228
5254
  if (typeof globalThis !== "undefined" && globalThis.__ripComponent) {
5229
- code += `const { isSignal, __pushComponent, __popComponent, setContext, getContext, hasContext, __cx__ } = globalThis.__ripComponent;
5255
+ code += `const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __Component } = globalThis.__ripComponent;
5230
5256
  `;
5231
5257
  } else {
5232
5258
  code += this.getComponentRuntime();
@@ -6699,9 +6725,10 @@ export default ${expr[1]}`;
6699
6725
  if (Array.isArray(params))
6700
6726
  params.forEach(extractPN);
6701
6727
  let bodyVars = this.collectFunctionVariables(body);
6702
- let newVars = new Set([...bodyVars].filter((v) => !this.programVars.has(v) && !this.reactiveVars?.has(v) && !paramNames.has(v)));
6728
+ let newVars = new Set([...bodyVars].filter((v) => !this.programVars.has(v) && !this.reactiveVars?.has(v) && !paramNames.has(v) && !this.scopeStack.some((s) => s.has(v))));
6703
6729
  let noRetStmts = ["return", "throw", "break", "continue"];
6704
6730
  let loopStmts = ["for-in", "for-of", "for-as", "while", "until", "loop"];
6731
+ this.scopeStack.push(new Set([...newVars, ...paramNames]));
6705
6732
  if (this.is(body, "block")) {
6706
6733
  let statements = this.unwrapBlock(body);
6707
6734
  if (hasExpansionParams && this.expansionAfterParams?.length > 0) {
@@ -6801,17 +6828,22 @@ export default ${expr[1]}`;
6801
6828
  }
6802
6829
  this.indentLevel--;
6803
6830
  code += this.indent() + "}";
6831
+ this.scopeStack.pop();
6804
6832
  this.sideEffectOnly = prevSEO;
6805
6833
  return code;
6806
6834
  }
6807
6835
  this.sideEffectOnly = prevSEO;
6836
+ let result;
6808
6837
  if (isConstructor || this.hasExplicitControlFlow(body))
6809
- return `{ ${this.generate(body, "statement")}; }`;
6810
- if (Array.isArray(body) && (noRetStmts.includes(body[0]) || loopStmts.includes(body[0])))
6811
- return `{ ${this.generate(body, "statement")}; }`;
6812
- if (sideEffectOnly)
6813
- return `{ ${this.generate(body, "statement")}; return; }`;
6814
- return `{ return ${this.generate(body, "value")}; }`;
6838
+ result = `{ ${this.generate(body, "statement")}; }`;
6839
+ else if (Array.isArray(body) && (noRetStmts.includes(body[0]) || loopStmts.includes(body[0])))
6840
+ result = `{ ${this.generate(body, "statement")}; }`;
6841
+ else if (sideEffectOnly)
6842
+ result = `{ ${this.generate(body, "statement")}; return; }`;
6843
+ else
6844
+ result = `{ return ${this.generate(body, "value")}; }`;
6845
+ this.scopeStack.pop();
6846
+ return result;
6815
6847
  }
6816
6848
  generateFunctionBody(body, params = [], sideEffectOnly = false) {
6817
6849
  return this.generateBodyWithReturns(body, params, { sideEffectOnly, hasExpansionParams: this.expansionAfterParams?.length > 0 });
@@ -7747,6 +7779,7 @@ const __primitiveCoercion = {
7747
7779
  };
7748
7780
 
7749
7781
  function __state(initialValue) {
7782
+ if (initialValue != null && typeof initialValue === 'object' && typeof initialValue.read === 'function') return initialValue;
7750
7783
  let value = initialValue;
7751
7784
  const subscribers = new Set();
7752
7785
  let notifying = false;
@@ -8041,8 +8074,8 @@ function getComponentRuntime() {
8041
8074
  return new CodeGenerator({}).getComponentRuntime();
8042
8075
  }
8043
8076
  // src/browser.js
8044
- var VERSION = "3.7.3";
8045
- var BUILD_DATE = "2026-02-11@10:29:15GMT";
8077
+ var VERSION = "3.7.4";
8078
+ var BUILD_DATE = "2026-02-11@13:07:41GMT";
8046
8079
  if (typeof globalThis !== "undefined" && !globalThis.__rip) {
8047
8080
  new Function(getReactiveRuntime())();
8048
8081
  }
@@ -8058,24 +8091,23 @@ async function processRipScripts() {
8058
8091
  continue;
8059
8092
  try {
8060
8093
  const ripCode = dedent(script.textContent);
8061
- const jsCode = compileToJS(ripCode);
8094
+ let jsCode;
8095
+ try {
8096
+ jsCode = compileToJS(ripCode);
8097
+ } catch (compileError) {
8098
+ console.error("Rip compile error:", compileError.message);
8099
+ console.error("Source:", ripCode);
8100
+ continue;
8101
+ }
8062
8102
  await (0, eval)(`(async()=>{
8063
8103
  ${jsCode}
8064
8104
  })()`);
8065
8105
  script.setAttribute("data-rip-processed", "true");
8066
8106
  } catch (error) {
8067
- console.error("Error compiling Rip script:", error);
8068
- console.error("Script content:", script.textContent);
8107
+ console.error("Rip runtime error:", error);
8069
8108
  }
8070
8109
  }
8071
8110
  }
8072
- if (typeof document !== "undefined") {
8073
- if (document.readyState === "loading") {
8074
- document.addEventListener("DOMContentLoaded", processRipScripts);
8075
- } else {
8076
- processRipScripts();
8077
- }
8078
- }
8079
8111
  async function importRip(url) {
8080
8112
  const source = await fetch(url).then((r) => {
8081
8113
  if (!r.ok)
@@ -8119,6 +8151,13 @@ if (typeof globalThis !== "undefined") {
8119
8151
  globalThis.importRip = importRip;
8120
8152
  globalThis.compileToJS = compileToJS;
8121
8153
  }
8154
+ if (typeof document !== "undefined") {
8155
+ if (document.readyState === "loading") {
8156
+ document.addEventListener("DOMContentLoaded", processRipScripts);
8157
+ } else {
8158
+ processRipScripts();
8159
+ }
8160
+ }
8122
8161
  export {
8123
8162
  rip,
8124
8163
  processRipScripts,