rip-lang 3.13.11 → 3.13.13

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
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <script type="module" src="../dist/rip.min.js"></script>
6
+ <script defer src="../dist/rip.min.js"></script>
7
7
  </head>
8
8
  <body>
9
9
  <div id="app"></div>
package/docs/index.html CHANGED
@@ -495,7 +495,7 @@
495
495
  body.light .repl-prompt-text { color: #007acc; }
496
496
  </style>
497
497
  <link rel="preload" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.js" as="script">
498
- <script type="module" src="dist/rip.min.js"></script>
498
+ <script defer src="dist/rip.min.js"></script>
499
499
  </head>
500
500
  <body>
501
501
 
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,500;0,700;1,400&family=Nothing+You+Could+Do&display=swap" rel="stylesheet">
10
- <script type="module" src="../dist/rip.min.js"></script>
10
+ <script defer src="../dist/rip.min.js" data-mount="Home"></script>
11
11
  <style>
12
12
  /* ==========================================================================
13
13
  Lab Results — Styles
@@ -596,7 +596,7 @@ body {
596
596
 
597
597
  <!-- ===== Components ===== -->
598
598
 
599
- <script type="text/rip" data-name="index">
599
+ <script type="text/rip">
600
600
  # Lab Results - Main Page
601
601
 
602
602
  export Home = component
@@ -758,7 +758,7 @@ export Home = component
758
758
  acceptable: (k, v) -> @acceptable(k, v)
759
759
  </script>
760
760
 
761
- <script type="text/rip" data-name="settings">
761
+ <script type="text/rip">
762
762
  # Settings - Form panel with two-way bound inputs
763
763
 
764
764
  export Settings = component
@@ -831,7 +831,7 @@ export Settings = component
831
831
  @click: -> window.print()
832
832
  </script>
833
833
 
834
- <script type="text/rip" data-name="brochure">
834
+ <script type="text/rip">
835
835
  # Brochure — Container composing all pages
836
836
 
837
837
  export Brochure = component
@@ -859,7 +859,7 @@ export Brochure = component
859
859
  DetailPage topic: topic, history: history, ranges: ranges, acceptable: acceptable
860
860
  </script>
861
861
 
862
- <script type="text/rip" data-name="cover">
862
+ <script type="text/rip">
863
863
  # Cover — Brochure cover page
864
864
 
865
865
  export Cover = component
@@ -900,7 +900,7 @@ export Cover = component
900
900
  div poweredBy
901
901
  </script>
902
902
 
903
- <script type="text/rip" data-name="summary">
903
+ <script type="text/rip">
904
904
  # Summary - Medical Summary table (Page 1)
905
905
 
906
906
  export Summary = component
@@ -996,7 +996,7 @@ export Summary = component
996
996
  td.value row.value
997
997
  </script>
998
998
 
999
- <script type="text/rip" data-name="detail-page">
999
+ <script type="text/rip">
1000
1000
  # DetailPage - Reusable health detail page with gradient header and gauges
1001
1001
 
1002
1002
  export DetailPage = component
@@ -1068,7 +1068,7 @@ export DetailPage = component
1068
1068
  .detail__footer-image style: "background-image: url(#{topic.image})"
1069
1069
  </script>
1070
1070
 
1071
- <script type="text/rip" data-name="gauge">
1071
+ <script type="text/rip">
1072
1072
  # Gauge — SVG semi-circular gauge with rotating needle
1073
1073
 
1074
1074
  export Gauge = component
@@ -32,7 +32,7 @@
32
32
  }
33
33
  .dot:hover { transform: scale(2.5); z-index: 1; }
34
34
  </style>
35
- <script type="module" src="https://shreeve.github.io/rip-lang/dist/rip.min.js"></script>
35
+ <script defer src="https://shreeve.github.io/rip-lang/dist/rip.min.js"></script>
36
36
  <script>
37
37
  function updateScale() {
38
38
  var s = Math.min(window.innerWidth / 1350, window.innerHeight / 1250, 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.13.11",
3
+ "version": "3.13.13",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
@@ -31,9 +31,8 @@
31
31
  "CHANGELOG.md"
32
32
  ],
33
33
  "scripts": {
34
- "build": "bun scripts/build-browser.js",
35
- "build:debug": "bun scripts/build-browser.js --debug",
36
- "bump": "bun scripts/bump-version.js",
34
+ "build": "bun scripts/build.js",
35
+ "bump": "bun scripts/bump.js",
37
36
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
38
37
  "serve": "bun scripts/serve.js",
39
38
  "test": "bun test/runner.js",
package/scripts/serve.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  // Simple static file server with brotli support
3
- import { readFileSync, existsSync } from 'fs';
3
+ import { readFileSync, existsSync, statSync } from 'fs';
4
4
  import { join, extname, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
 
@@ -37,6 +37,11 @@ function handleRequest(req) {
37
37
  pathname = pathname.slice('/rip-lang'.length);
38
38
  }
39
39
 
40
+ // Redirect /dir to /dir/ if it's a directory
41
+ if (!pathname.endsWith('/') && !extname(pathname)) {
42
+ try { if (statSync(join(ROOT, pathname)).isDirectory()) return Response.redirect(pathname + '/', 301); } catch {}
43
+ }
44
+
40
45
  // Default to index.html for directory requests
41
46
  if (pathname.endsWith('/')) {
42
47
  pathname += 'index.html';
package/src/app.rip CHANGED
@@ -876,11 +876,9 @@ connectWatch = (url) ->
876
876
  # ==============================================================================
877
877
  # Launch — fetch bundle, create stash, wire router + renderer, start app
878
878
  #
879
- # Entry point for Rip applications. Handles four bundle sources:
879
+ # Entry point for Rip applications. Handles two bundle sources:
880
880
  # 1. Explicit bundle object (opts.bundle)
881
- # 2. Array of component URLs (opts.components)
882
- # 3. Inline <script type="text/rip" data-name="..."> tags
883
- # 4. Server fetch from {appBase}/bundle
881
+ # 2. Fetch from URL (opts.bundleUrl) — used by serve middleware's data-launch
884
882
  #
885
883
  # Creates the app stash, sets up persistence if configured, builds the
886
884
  # component resolver, initializes the router and renderer, and optionally
@@ -908,33 +906,15 @@ export launch = (appBase = '', opts = {}) ->
908
906
  el.id = target.replace(/^#/, '')
909
907
  document.body.prepend el
910
908
 
911
- # Get the app bundle — explicit object, literal URL, static files, inline DOM, or server fetch
909
+ # Get the app bundle — explicit object or fetch from URL
912
910
  if opts.bundle
913
911
  bundle = opts.bundle
914
912
  else if opts.bundleUrl
915
913
  res = await fetch(opts.bundleUrl, cache: 'no-cache')
916
914
  throw new Error "launch: #{opts.bundleUrl} (#{res.status})" unless res.ok
917
915
  bundle = res.json!
918
- else if opts.components and Array.isArray(opts.components)
919
- components = {}
920
- for url in opts.components
921
- res = await fetch(url)
922
- if res.ok
923
- name = url.split('/').pop()
924
- components["components/#{name}"] = await res.text()
925
- bundle = { components, data: {} }
926
- else if typeof document isnt 'undefined' and document.querySelectorAll('script[type="text/rip"][data-name]').length > 0
927
- components = {}
928
- for script in document.querySelectorAll('script[type="text/rip"][data-name]')
929
- name = script.getAttribute('data-name')
930
- name += '.rip' unless name.endsWith('.rip')
931
- components["components/#{name}"] = script.textContent
932
- bundle = { components, data: {} }
933
916
  else
934
- bundleUrl = "#{appBase}/bundle"
935
- res = await fetch(bundleUrl, cache: 'no-cache')
936
- throw new Error "launch: #{bundleUrl} (#{res.status})" unless res.ok
937
- bundle = res.json!
917
+ throw new Error "launch: no bundle or bundleUrl provided"
938
918
 
939
919
  # Create the unified stash
940
920
  app = stash { components: {}, routes: {}, data: {} }
package/src/browser.js CHANGED
@@ -13,11 +13,11 @@ export const BUILD_DATE = "0000-00-00@00:00:00GMT";
13
13
  // Import compiler functions for use in rip() function and globalThis registration
14
14
  import { compile, compileToJS, formatSExpr, getReactiveRuntime, getComponentRuntime } from './compiler.js';
15
15
 
16
- // Eagerly register Rip's reactive primitives on globalThis so that
17
- // framework code (app.rip) can use them directly without the compiler
18
- // needing to detect reactive operators in the source
19
- if (typeof globalThis !== 'undefined' && !globalThis.__rip) {
20
- new Function(getReactiveRuntime())();
16
+ // Eagerly register Rip's reactive and component runtimes on globalThis so that
17
+ // framework code (app.rip) and browser-compiled scripts can use them directly
18
+ if (typeof globalThis !== 'undefined') {
19
+ if (!globalThis.__rip) new Function(getReactiveRuntime())();
20
+ if (!globalThis.__ripComponent) new Function(getComponentRuntime())();
21
21
  }
22
22
 
23
23
  const dedent = s => {
@@ -26,43 +26,88 @@ const dedent = s => {
26
26
  return s.replace(RegExp(`^[ \t]{${i}}`, 'gm'), '').trim();
27
27
  }
28
28
 
29
- // Browser runtime for executing <script type="text/rip"> tags
30
- // Supports both inline scripts and external files via src attribute
29
+ // Browser runtime: collect all <script type="text/rip"> sources (inline + src)
30
+ // plus any data-src URLs on the runtime tag, compile them all with shared-scope
31
+ // options, and execute as one async IIFE. Then handle data-launch for server mode.
31
32
  async function processRipScripts() {
32
- const scripts = document.querySelectorAll('script[type="text/rip"]');
33
-
34
- for (const script of scripts) {
35
- if (script.hasAttribute('data-rip-processed')) continue;
36
- if (script.hasAttribute('data-name')) continue;
37
-
38
- try {
39
- let ripCode;
40
- if (script.src) {
41
- const response = await fetch(script.src);
42
- if (!response.ok) {
43
- console.error(`Rip: failed to fetch ${script.src} (${response.status})`);
44
- continue;
33
+ const sources = [];
34
+
35
+ // Step 1: Collect data-src URLs from the runtime script tag
36
+ const runtimeTag = document.querySelector('script[src$="rip.min.js"], script[src$="rip.js"]');
37
+ const dataSrc = runtimeTag?.getAttribute('data-src');
38
+ if (dataSrc) {
39
+ for (const url of dataSrc.trim().split(/\s+/)) {
40
+ if (url) sources.push({ url });
41
+ }
42
+ }
43
+
44
+ // Step 2: Collect all <script type="text/rip"> tags (inline and external)
45
+ for (const script of document.querySelectorAll('script[type="text/rip"]')) {
46
+ if (script.src) {
47
+ sources.push({ url: script.src });
48
+ } else {
49
+ const code = dedent(script.textContent);
50
+ if (code) sources.push({ code });
51
+ }
52
+ }
53
+
54
+ // Step 3: Fetch externals, compile all, execute in shared scope
55
+ if (sources.length > 0) {
56
+ await Promise.all(sources.map(async (s) => {
57
+ if (!s.url) return;
58
+ try {
59
+ const res = await fetch(s.url);
60
+ if (!res.ok) {
61
+ console.error(`Rip: failed to fetch ${s.url} (${res.status})`);
62
+ return;
45
63
  }
46
- ripCode = await response.text();
47
- } else {
48
- ripCode = dedent(script.textContent);
64
+ s.code = await res.text();
65
+ } catch (e) {
66
+ console.error(`Rip: failed to fetch ${s.url}:`, e.message);
49
67
  }
68
+ }));
50
69
 
51
- let jsCode;
70
+ const opts = { skipRuntimes: true, skipExports: true };
71
+ const compiled = [];
72
+ for (const s of sources) {
73
+ if (!s.code) continue;
52
74
  try {
53
- jsCode = compileToJS(ripCode);
54
- } catch (compileError) {
55
- console.error('Rip compile error:', compileError.message);
56
- console.error('Source:', ripCode);
57
- continue;
75
+ compiled.push(compileToJS(s.code, opts));
76
+ } catch (e) {
77
+ console.error('Rip compile error:', e.message);
58
78
  }
79
+ }
59
80
 
60
- // Execute as async to support await (importRip!, etc.)
61
- await (0, eval)(`(async()=>{\n${jsCode}\n})()`);
81
+ if (compiled.length > 0) {
82
+ let js = compiled.join('\n');
62
83
 
63
- script.setAttribute('data-rip-processed', 'true');
64
- } catch (error) {
65
- console.error('Rip runtime error:', error);
84
+ // Step 4: Append data-mount call inside the shared IIFE
85
+ const mount = runtimeTag?.getAttribute('data-mount');
86
+ if (mount) {
87
+ const target = runtimeTag.getAttribute('data-target') || 'body';
88
+ js += `\n${mount}.mount(${JSON.stringify(target)});`;
89
+ }
90
+
91
+ try {
92
+ await (0, eval)(`(async()=>{\n${js}\n})()`);
93
+ } catch (e) {
94
+ console.error('Rip runtime error:', e);
95
+ }
96
+ }
97
+ }
98
+
99
+ // Step 5: data-launch triggers launch() for server mode
100
+ const cfg = document.querySelector('script[data-launch]');
101
+ if (cfg && !globalThis.__ripLaunched) {
102
+ const ui = importRip.modules?.['app.rip'];
103
+ if (ui?.launch) {
104
+ const url = cfg.getAttribute('data-launch') || '';
105
+ const hash = cfg.getAttribute('data-hash');
106
+ const persist = cfg.getAttribute('data-persist');
107
+ const opts = { hash: hash !== 'false' };
108
+ if (url) opts.bundleUrl = url;
109
+ if (persist != null) opts.persist = persist === 'local' ? 'local' : true;
110
+ await ui.launch('', opts);
66
111
  }
67
112
  }
68
113
  }
@@ -134,28 +179,12 @@ if (typeof globalThis !== 'undefined') {
134
179
  globalThis.__ripExports = { compile, compileToJS, formatSExpr, getStdlibCode, VERSION, BUILD_DATE, getReactiveRuntime, getComponentRuntime };
135
180
  }
136
181
 
137
- // Auto-launch: requires data-url or inline data-name scripts.
138
- // data-url is the literal fetch URL for the component bundle.
139
- async function autoLaunch() {
140
- if (globalThis.__ripLaunched) return;
141
- const ui = importRip.modules?.['app.rip'];
142
- if (!ui?.launch) return;
143
- const cfg = document.querySelector('script[data-url], script[data-hash]');
144
- const tag = document.querySelectorAll('script[type="text/rip"][data-name]').length > 0;
145
- if (!cfg && !tag) return;
146
- const url = cfg?.getAttribute('data-url') || '';
147
- const hash = cfg?.getAttribute('data-hash');
148
- const opts = { hash: hash !== 'false' };
149
- if (url) opts.bundleUrl = url;
150
- await ui.launch('', opts);
151
- }
152
-
153
- // Auto-process <script type="text/rip"> blocks, then auto-launch if applicable.
182
+ // Auto-process <script type="text/rip"> blocks and handle data-launch.
154
183
  // Deferred via queueMicrotask so bundled entry code (e.g. rip.min.js registering
155
184
  // importRip.modules) runs before script processing begins.
156
185
  if (typeof document !== 'undefined') {
157
186
  globalThis.__ripScriptsReady = new Promise(resolve => {
158
- const run = () => processRipScripts().then(autoLaunch).then(resolve);
187
+ const run = () => processRipScripts().then(resolve);
159
188
  if (document.readyState === 'loading') {
160
189
  document.addEventListener('DOMContentLoaded', () => queueMicrotask(run));
161
190
  } else {
package/src/compiler.js CHANGED
@@ -636,6 +636,7 @@ export class CodeGenerator {
636
636
  }
637
637
 
638
638
  let skip = this.options.skipPreamble;
639
+ let skipRT = this.options.skipRuntimes;
639
640
 
640
641
  if (!skip) {
641
642
 
@@ -645,10 +646,12 @@ export class CodeGenerator {
645
646
  needsBlank = true;
646
647
 
647
648
  // On-demand helpers — only emitted when referenced
648
- if (this.helpers.has('slice' )) { code += 'const slice = [].slice;\n'; needsBlank = true; }
649
- if (this.helpers.has('modulo' )) { code += 'const modulo = (n, d) => { n = +n; d = +d; return (n % d + d) % d; };\n'; needsBlank = true; }
649
+ // Use var when skipRuntimes is set so helpers can be safely re-emitted across concatenated files
650
+ let helperDecl = skipRT ? 'var' : 'const';
651
+ if (this.helpers.has('slice' )) { code += `${helperDecl} slice = [].slice;\n`; needsBlank = true; }
652
+ if (this.helpers.has('modulo' )) { code += `${helperDecl} modulo = (n, d) => { n = +n; d = +d; return (n % d + d) % d; };\n`; needsBlank = true; }
650
653
  if (this.helpers.has('toMatchable')) {
651
- code += 'const toMatchable = (v, allowNewlines) => {\n';
654
+ code += `${helperDecl} toMatchable = (v, allowNewlines) => {\n`;
652
655
  code += ' if (typeof v === "string") return !allowNewlines && /[\\n\\r]/.test(v) ? null : v;\n';
653
656
  code += ' if (v == null) return "";\n';
654
657
  code += ' if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") return String(v);\n';
@@ -673,7 +676,9 @@ export class CodeGenerator {
673
676
  }
674
677
 
675
678
  if (this.usesReactivity && !skip) {
676
- if (typeof globalThis !== 'undefined' && globalThis.__rip) {
679
+ if (skipRT) {
680
+ code += 'var { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
681
+ } else if (typeof globalThis !== 'undefined' && globalThis.__rip) {
677
682
  code += 'const { __state, __computed, __effect, __batch, __readonly, __setErrorHandler, __handleError, __catchErrors } = globalThis.__rip;\n';
678
683
  } else {
679
684
  code += this.getReactiveRuntime();
@@ -682,7 +687,9 @@ export class CodeGenerator {
682
687
  }
683
688
 
684
689
  if (this.usesTemplates && !skip) {
685
- if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
690
+ if (skipRT) {
691
+ code += 'var { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __Component } = globalThis.__ripComponent;\n';
692
+ } else if (typeof globalThis !== 'undefined' && globalThis.__ripComponent) {
686
693
  code += 'const { __pushComponent, __popComponent, setContext, getContext, hasContext, __clsx, __Component } = globalThis.__ripComponent;\n';
687
694
  } else {
688
695
  code += this.getComponentRuntime();
@@ -1601,6 +1608,8 @@ export class CodeGenerator {
1601
1608
 
1602
1609
  if (rest.length === 3) tryCode += ' finally ' + this.generate(rest[2], 'statement');
1603
1610
 
1611
+ if (rest.length === 1) tryCode += ' catch {}';
1612
+
1604
1613
  if (needsReturns) {
1605
1614
  let isAsync = this.containsAwait(rest[0]) || (rest[1] && this.containsAwait(rest[1]));
1606
1615
  return `(${isAsync ? 'async ' : ''}() => { ${tryCode} })()`;
@@ -2024,6 +2033,11 @@ export class CodeGenerator {
2024
2033
 
2025
2034
  generateExport(head, rest) {
2026
2035
  let [decl] = rest;
2036
+ if (this.options.skipExports) {
2037
+ if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return '';
2038
+ if (this.is(decl, '=')) return `const ${decl[1]} = ${this.generate(decl[2], 'value')}`;
2039
+ return this.generate(decl, 'statement');
2040
+ }
2027
2041
  if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return `export { ${decl.join(', ')} }`;
2028
2042
  if (this.is(decl, '=')) return `export const ${decl[1]} = ${this.generate(decl[2], 'value')}`;
2029
2043
  return `export ${this.generate(decl, 'statement')}`;
@@ -2031,6 +2045,10 @@ export class CodeGenerator {
2031
2045
 
2032
2046
  generateExportDefault(head, rest) {
2033
2047
  let [expr] = rest;
2048
+ if (this.options.skipExports) {
2049
+ if (this.is(expr, '=')) return `const ${expr[1]} = ${this.generate(expr[2], 'value')}`;
2050
+ return this.generate(expr, 'statement');
2051
+ }
2034
2052
  if (this.is(expr, '=')) {
2035
2053
  return `const ${expr[1]} = ${this.generate(expr[2], 'value')};\nexport default ${expr[1]}`;
2036
2054
  }
@@ -2038,10 +2056,12 @@ export class CodeGenerator {
2038
2056
  }
2039
2057
 
2040
2058
  generateExportAll(head, rest) {
2059
+ if (this.options.skipExports) return '';
2041
2060
  return `export * from ${this.addJsExtensionAndAssertions(rest[0])}`;
2042
2061
  }
2043
2062
 
2044
2063
  generateExportFrom(head, rest) {
2064
+ if (this.options.skipExports) return '';
2045
2065
  let [specifiers, source] = rest;
2046
2066
  let fixedSource = this.addJsExtensionAndAssertions(source);
2047
2067
  if (Array.isArray(specifiers)) {
@@ -3226,6 +3246,8 @@ export class Compiler {
3226
3246
  let generator = new CodeGenerator({
3227
3247
  dataSection,
3228
3248
  skipPreamble: this.options.skipPreamble,
3249
+ skipRuntimes: this.options.skipRuntimes,
3250
+ skipExports: this.options.skipExports,
3229
3251
  reactiveVars: this.options.reactiveVars,
3230
3252
  sourceMap,
3231
3253
  });
package/src/components.js CHANGED
@@ -570,8 +570,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
570
570
  }
571
571
 
572
572
  // Dot access: transform the object but not the property name
573
- if (sexpr[0] === '.') {
574
- return ['.', this.transformComponentMembers(sexpr[1]), sexpr[2]];
573
+ if (sexpr[0] === '.' || sexpr[0] === '?.') {
574
+ return [sexpr[0], this.transformComponentMembers(sexpr[1]), sexpr[2]];
575
575
  }
576
576
 
577
577
  // Force thin arrows to fat arrows inside components to preserve this binding
@@ -1914,6 +1914,9 @@ class __Component {
1914
1914
  this._root.parentNode.removeChild(this._root);
1915
1915
  }
1916
1916
  }
1917
+ static mount(target = 'body') {
1918
+ return new this().mount(target);
1919
+ }
1917
1920
  }
1918
1921
 
1919
1922
  // Register on globalThis for runtime deduplication
@@ -1,126 +0,0 @@
1
- # Rip UI — Roadmap
2
-
3
- ## What's There
4
-
5
- ### Core Reactive System
6
- - **Reactive primitives** — `:=` (state), `~=` (computed), `~>` (effects) as
7
- language syntax. Fine-grained dependency tracking, batching, readonly (`=!`).
8
- - **`__Component` base class** — mount, unmount, context push/pop, constructor
9
- lifecycle all handled in the runtime. Components just override `_init`.
10
- - **`__state` signal passthrough** — if a value is already a signal, `__state`
11
- returns it as-is. No separate `isSignal` check needed.
12
- - **Fine-grained DOM** — `_create` builds real DOM nodes, `_setup` wires
13
- reactive effects that update individual text nodes and attributes.
14
- - **Timing primitives** — `delay`, `debounce`, `throttle`, `hold` as small
15
- user-space functions composing `:=` + `~>` + cleanup. No framework API.
16
-
17
- ### Component Model
18
- - **Component composition** — PascalCase identifiers in render blocks
19
- instantiate child components. `card.rip` → `Card`. App-scoped, lazy-compiled,
20
- cached after first use. No imports needed.
21
- - **Reactive props** — parent passes `:=` signals directly to children.
22
- Child's `__state` passthrough returns the signal as-is. Two-way binding.
23
- - **Readonly props** — `=!` for props that children can read but not write.
24
- - **Children blocks** — `Card title: "Hello" -> p "content"` passes children
25
- as a DOM node via the `@children` slot. `#content` for layout slots.
26
- - **Unmount cascade** — parent tracks child instances in `_children`.
27
- `unmount()` cascades depth-first.
28
- - **Conditional rendering** — `if`/`else` in render blocks with anchor-based
29
- conditional DOM.
30
- - **List rendering** — `for item in items` with keyed reconciliation.
31
- - **Event handling** — `@click: method` binds to `this.method` correctly.
32
- - **CSS classes** — `div.counter.active` compiles to static className.
33
- Dynamic classes via `__clsx(...)`.
34
- - **Context** — `setContext`/`getContext`/`hasContext` for sharing data
35
- between ancestor and descendant components without prop drilling.
36
- - **Lifecycle hooks** — `mounted`, `unmounted` work. `beforeMount`,
37
- `beforeUnmount` are recognized.
38
-
39
- ### State & Routing
40
- - **Reactive stash** — shared reactive store with proxy-based access.
41
- - **File-based router** — URL-to-component mapping with params, guards,
42
- layouts, `_navigating` signal, keep-alive component cache.
43
- - **Hash routing** — `launch '/app', hash: true` for static single-file
44
- deployment. Uses `readUrl()`/`writeUrl()` helpers. Back/forward and
45
- direct URL loading work correctly.
46
- - **State persistence** — `persist: true` enables debounced auto-save of
47
- `app.data` to sessionStorage. `_writeVersion` signal + `beforeunload` safety.
48
- - **Error boundaries** — catch mount-time errors.
49
-
50
- ### Infrastructure
51
- - **Component store** — in-memory `.rip` file storage with compilation cache
52
- and file watchers for hot reload via SSE.
53
- - **`launch bundle:`** — inline all components as heredoc strings in a single
54
- HTML file. Zero-server deployment. `docs/demo.html` is a 337-line example.
55
- - **Combined bundle** — `rip-ui.min.js` (~52KB Brotli) packages the compiler
56
- and pre-compiled UI framework in one file. Eliminates the `ui.rip` fetch
57
- and its runtime compilation. `importRip('ui.rip')` is intercepted and returns
58
- the pre-compiled module instantly.
59
- - **Parallel loading** — Monaco Editor preloaded via `<link rel="preload">`,
60
- compiler exports available instantly via `globalThis.__ripExports`. All
61
- synchronous setup runs in parallel with the Monaco CDN fetch.
62
- - **FOUC prevention** — playground pages use `body { opacity: 0 }` with a
63
- `body.ready` fade-in transition after full initialization.
64
- - **Runtime deduplication** — both runtimes register on `globalThis.__rip`
65
- and `globalThis.__ripComponent`. Multiple compilations share one runtime.
66
- - **Smooth app launch** — container fades in after first mount + font load.
67
- - **Navigation anti-flicker** — `_navigating` uses `delay 100` to suppress
68
- brief loading indicators.
69
-
70
- ---
71
-
72
- ## Where Rip UI Wins
73
-
74
- 1. **Simplicity of the reactive model.** Three operators, minimal complete set. No hooks rules, no dependency arrays, no `.value` papercuts.
75
- 2. **Zero-build development.** No other framework runs entirely in the browser with zero build tooling.
76
- 3. **Timing primitives from composition.** `delay`, `debounce`, `throttle`, `hold` prove architectural correctness — hard problems dissolve into small functions.
77
- 4. **Effect cleanup design.** Returning a function from an effect for cleanup is the cleanest pattern across all frameworks.
78
- 5. **Syntax over API.** `:=` beats `ref()`. `~=` beats `computed()`. `~>` beats `watchEffect()`. Less ceremony, same power.
79
- 6. **Static deployment.** Hash routing + `launch bundle:` = full SPA in a single HTML file on any static host.
80
-
81
- ## Where Others Win
82
-
83
- 1. **Ecosystem.** Zero component libraries, zero third-party integrations, zero community packages.
84
- 2. **SSR.** No server-side rendering means no SEO, no progressive enhancement.
85
- 3. **Performance proof.** No benchmarks, no published numbers.
86
- 4. **TypeScript depth.** Framework exports have no `.d.ts` files.
87
- 5. **Tooling.** No DevTools extension, no CLI scaffolding.
88
- 6. **Battle-testing.** React serves billions. We serve a demo app.
89
-
90
- ---
91
-
92
- ## The Path Forward
93
-
94
- ### Must-have (blocks adoption)
95
- 1. Named slots — multiple content projection points (header, body, footer)
96
- 2. Framework `.d.ts` files — TypeScript definitions for all exports
97
-
98
- ### Should-have (competitive parity)
99
- 3. AOT compilation path — component-level ahead-of-time for production (framework AOT done in v0.3.2)
100
- 4. SSR — server rendering for SEO
101
- 5. js-framework-benchmark — published performance numbers
102
- 6. Keyed list reconciliation — optimized array diffing for large datasets
103
-
104
- ### Nice-to-have (ecosystem growth)
105
- 7. DevTools extension — visual component/state inspector
106
- 8. `create-rip-app` CLI — project scaffolding
107
- 9. Headless UI primitives — accessible dropdown, modal, dialog
108
- 10. State-preserving HMR — keep reactive state during hot reload
109
- 11. Scoped slots and teleport — advanced composition patterns
110
-
111
- ---
112
-
113
- ## Known Caveats
114
-
115
- - **`getCompiled`/`setCompiled` cache modules, not JS strings.** Same source
116
- at two paths = two cached modules. No deduplication across paths.
117
- - **Stash proxy missing `getOwnPropertyDescriptor` trap.** Works in practice
118
- but could cause issues with `Object.keys`/spread in strict environments.
119
- - **Router regex recreated on every `buildRoutes` call.** Not a problem with
120
- small route tables. Could optimize with fingerprint comparison.
121
- - **`router.current` creates a new object on every read.** A cached object
122
- that updates inside batch would reduce garbage.
123
- - **REPL reactive state doesn't persist across `rip()` calls.** Each call is
124
- a separate compilation. All reactive code must be in a single call.
125
- - **Hash routing and path routing are mutually exclusive.** Set once at
126
- `launch` time. No runtime switching between modes.