rip-lang 3.15.4 → 3.16.1

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.
Files changed (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/docs/ui/index.css CHANGED
@@ -130,6 +130,11 @@ button { font: inherit; cursor: pointer; }
130
130
  &[data-selected] { font-weight: 600; color: #18181b; }
131
131
  }
132
132
 
133
+ .gallery [role="status"][data-empty] {
134
+ display: block; padding: 8px 12px; border-radius: 4px;
135
+ font-size: 14px; line-height: 21px; color: #71717a;
136
+ }
137
+
133
138
  /* ── Combobox clear button ── */
134
139
 
135
140
  .gallery [data-clear] {
@@ -260,8 +265,18 @@ button { font: inherit; cursor: pointer; }
260
265
  display: inline-flex; align-items: center; gap: 8px;
261
266
  padding: 6px 12px; border: 1px solid #d4d4d8; border-radius: 12px;
262
267
  background: white; cursor: pointer; font-size: 14px;
268
+ }
269
+
270
+ .gallery [role="checkbox"][data-checked] {
271
+ background: #fafafa; border-color: #18181b; color: #18181b;
272
+ }
273
+
274
+ .gallery [role="switch"] {
275
+ border-radius: 999px;
276
+ }
263
277
 
264
- &[data-checked] { background: #fafafa; border-color: #18181b; color: #18181b; }
278
+ .gallery [role="switch"][data-checked] {
279
+ background: #dbeafe; border-color: #60a5fa; color: #1d4ed8;
265
280
  }
266
281
 
267
282
  /* ── Menu ── */
@@ -612,26 +627,32 @@ select.rip-grid-editor { cursor: pointer; padding-left: 4px; }
612
627
  /* ── MultiSelect ── */
613
628
 
614
629
  .gallery [data-chips] {
615
- display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
616
- padding: 4px 8px; border: 1px solid #d4d4d8; border-radius: 12px;
617
- background: white; min-height: 38px; cursor: text;
630
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
631
+ padding: 6px 12px; border: 1px solid #d4d4d8; border-radius: 16px;
632
+ background: white; min-height: 42px; cursor: text;
618
633
 
619
634
  &:focus-within { border-color: #18181b; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }
620
635
 
621
- input {
622
- border: none; outline: none; font: inherit; min-width: 80px;
623
- flex: 1; padding: 4px 0; background: transparent;
636
+ input,
637
+ input[type="text"] {
638
+ appearance: none; -webkit-appearance: none;
639
+ border: none !important; outline: none; box-shadow: none !important; border-radius: 0 !important;
640
+ font: inherit; font-size: 14px; line-height: 1.2;
641
+ min-width: 88px; margin: 0;
642
+ flex: 1 1 120px; height: 28px; min-height: 28px; padding: 0 2px !important;
643
+ background: transparent !important;
624
644
  }
625
645
  }
626
646
 
627
647
  .gallery [data-chip] {
628
648
  display: inline-flex; align-items: center; gap: 4px;
629
- padding: 2px 8px; background: #fafafa; color: #18181b;
630
- border-radius: 4px; font-size: 12px; font-weight: 500;
649
+ min-height: 28px; padding: 0 10px; background: #fafafa; color: #18181b;
650
+ border: 1px solid #e4e4e7; border-radius: 10px; font-size: 12px; font-weight: 500;
631
651
 
632
652
  [data-remove] {
633
653
  border: none; background: none; color: #18181b; cursor: pointer;
634
- font-size: 11px; padding: 0 2px; line-height: 1; border-radius: 2px;
654
+ width: 16px; height: 16px; font-size: 11px; padding: 0; line-height: 1; border-radius: 999px;
655
+ display: inline-flex; align-items: center; justify-content: center;
635
656
 
636
657
  &:hover { background: #f4f4f5; }
637
658
  }
@@ -767,6 +788,11 @@ select.rip-grid-editor { cursor: pointer; padding-left: 4px; }
767
788
  animation: fadeIn 0.15s ease;
768
789
  }
769
790
 
791
+ .gallery #preview-card [data-content] {
792
+ padding: 0; margin: 0; border: none; border-radius: 0;
793
+ background: transparent; box-shadow: none; min-width: 0;
794
+ }
795
+
770
796
  /* ── Menubar ── */
771
797
 
772
798
  .gallery [role="menubar"] {
@@ -946,7 +972,7 @@ html { scroll-behavior: auto; }
946
972
  html.no-transition, html.no-transition * { transition: none !important; }
947
973
 
948
974
  .toc {
949
- display: grid; grid-template-columns: 1fr 1fr;
975
+ display: grid; grid-template-columns: 58% 42%;
950
976
  margin-bottom: 36px;
951
977
  background: white; border: 1px solid #e4e4e7; border-radius: 12px;
952
978
  overflow: hidden;
@@ -954,14 +980,14 @@ html.no-transition, html.no-transition * { transition: none !important; }
954
980
 
955
981
  .toc-nav {
956
982
  display: flex; flex-wrap: wrap; align-content: flex-start;
957
- gap: 12px 20px; padding: 16px 20px;
983
+ gap: 14px 24px; padding: 18px 22px;
958
984
  }
959
985
 
960
- .toc-group { display: flex; flex-direction: column; gap: 3px; }
986
+ .toc-group { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
961
987
 
962
988
  .toc-label {
963
989
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
964
- text-transform: uppercase; color: #71717a; margin-bottom: 2px;
990
+ text-transform: uppercase; color: #71717a; margin-bottom: 3px;
965
991
  }
966
992
 
967
993
  .toc-search {
@@ -975,11 +1001,14 @@ html.no-transition, html.no-transition * { transition: none !important; }
975
1001
  }
976
1002
 
977
1003
  .toc a {
1004
+ display: inline-flex; align-items: center; width: fit-content; max-width: 100%;
978
1005
  font-size: 12px; color: #3f3f46; text-decoration: none;
979
- padding: 2px 8px; border-radius: 4px; background: #f5f5f5;
980
- cursor: pointer; transition: transform 0.12s; outline: none;
1006
+ padding: 6px 10px; border-radius: 10px; background: #f5f5f5;
1007
+ border: 1px solid transparent; cursor: pointer; outline: none;
1008
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
981
1009
 
982
- &:hover, &[data-active] { background: #f4f4f5; color: #18181b; transform: scale(1.05); }
1010
+ &:hover { background: #f4f4f5; border-color: #e4e4e7; color: #18181b; }
1011
+ &[data-active] { background: #f4f4f5; border-color: #d4d4d8; color: #18181b; box-shadow: inset 0 0 0 1px rgba(24,24,27,0.04); }
983
1012
  &:focus-visible { background: #f4f4f5; color: #18181b; outline: 2px solid #18181b; outline-offset: 1px; }
984
1013
  &[data-hidden] { display: none; }
985
1014
  }
@@ -1344,17 +1373,29 @@ body:has(.source-overlay) { overflow: hidden; }
1344
1373
 
1345
1374
  /* ── Collapsible ── */
1346
1375
 
1347
- .gallery [data-trigger]:has(+ [data-content]) {
1376
+ .gallery .collapsible-demo [data-trigger]:has(+ [data-content]) {
1348
1377
  padding: 8px 14px; border: 1px solid #d4d4d8; border-radius: 12px;
1349
1378
  background: white; cursor: pointer; font-size: 14px; width: 100%; text-align: left;
1379
+ transition: border-color 0.15s ease, background-color 0.15s ease, border-radius 0.15s ease;
1350
1380
 
1351
- &[aria-expanded="true"] { border-color: #71717a; }
1381
+ &[aria-expanded="true"] { border-color: #a1a1aa; }
1352
1382
  &:hover { background: #fafafa; }
1353
1383
  }
1354
1384
 
1355
1385
  .gallery .collapsible-demo [data-content] {
1356
1386
  padding: 12px 14px; color: #52525b; font-size: 13px;
1357
- border: 1px solid #e4e4e7; border-top: none; border-radius: 0 0 6px 6px;
1387
+ margin-top: -1px;
1388
+ border: 1px solid #e4e4e7; border-top: none; border-radius: 0 0 12px 12px;
1389
+ background: white;
1390
+ }
1391
+
1392
+ .gallery .collapsible-demo [data-open] [data-trigger]:has(+ [data-content]) {
1393
+ border-bottom-left-radius: 0;
1394
+ border-bottom-right-radius: 0;
1395
+ }
1396
+
1397
+ .gallery .collapsible-demo [data-open] [data-content] {
1398
+ border-color: #a1a1aa;
1358
1399
  }
1359
1400
 
1360
1401
  /* ── Pagination ── */
@@ -1460,8 +1501,9 @@ body:has(.source-overlay) { overflow: hidden; }
1460
1501
  .toc-search { border-color: #334155; background: #0f172a; color: #e4e4e7; }
1461
1502
  .toc-search::placeholder { color: #52525b; }
1462
1503
  .toc-label { color: #52525b; }
1463
- .toc a { color: #71717a; background: #0f172a; }
1464
- .toc a:hover, .toc a[data-active] { background: #172554; color: #71717a; }
1504
+ .toc a { color: #71717a; background: #0f172a; border-color: transparent; }
1505
+ .toc a:hover { background: #172554; border-color: #1e3a8a; color: #a1a1aa; }
1506
+ .toc a[data-active] { background: #172554; border-color: #1e3a8a; color: #e4e4e7; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04); }
1465
1507
  .toc-detail { border-left-color: #334155; background: #0f172a; }
1466
1508
  .toc-detail h3 { color: #e4e4e7; }
1467
1509
  .toc-detail .toc-desc { color: #71717a; }
@@ -1516,7 +1558,8 @@ body:has(.source-overlay) { overflow: hidden; }
1516
1558
 
1517
1559
  /* Checkbox / Switch */
1518
1560
  .gallery [role="checkbox"], .gallery [role="switch"] { border-color: #3f3f46; background: #1e293b; color: #e4e4e7; }
1519
- .gallery [role="checkbox"][data-checked], .gallery [role="switch"][data-checked] { background: #172554; border-color: #18181b; color: #71717a; }
1561
+ .gallery [role="checkbox"][data-checked] { background: #172554; border-color: #18181b; color: #a1a1aa; }
1562
+ .gallery [role="switch"][data-checked] { background: #1d4ed8; border-color: #2563eb; color: #eff6ff; }
1520
1563
 
1521
1564
  /* Menu */
1522
1565
  .gallery [aria-haspopup="menu"] { border-color: #3f3f46; background: #1e293b; color: #e4e4e7; }
@@ -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" data-mount="WidgetGallery" data-reload>
12
+ <script defer src="../dist/rip.min.js" data-src="bundle.json" data-mount="WidgetGallery">
13
13
  </script>
14
14
  </head>
15
15
  <body>
@@ -103,8 +103,9 @@ export WidgetGallery = component
103
103
  return unless entry
104
104
  sourceName = entry.name
105
105
  sourceLines = entry.lines
106
- resp = fetch! "components/#{id}.rip"
107
- sourceCode = resp.text!
106
+ src = window.__RIP__?.components?.read("_pkg/ui/#{id}.rip")
107
+ return unless src
108
+ sourceCode = src
108
109
  _closeSource: -> sourceCode = null
109
110
  _stopProp: (e) -> e.stopPropagation()
110
111
  _ensureHljs: (cb) ->
@@ -228,7 +229,7 @@ export WidgetGallery = component
228
229
  tocData := [
229
230
  { id: 'select', name: 'Select', lines: 135, desc: 'Dropdown with typeahead, keyboard nav, ARIA listbox.', props: ['@value', '@placeholder', '@disabled'], events: ['change'], keys: ['↕', 'Enter', 'Space', 'Esc', 'Home', 'End', 'type-ahead'], attrs: ['$open', '$placeholder', '$disabled', '$value', '$highlighted', '$selected'] }
230
231
  { id: 'combobox', name: 'Combobox', lines: 118, desc: 'Filterable input + listbox for search-as-you-type.', props: ['@query', '@items', '@placeholder', '@disabled', '@autoHighlight'], events: ['filter', 'select'], keys: ['↕', 'Enter', 'Esc', 'Tab'], attrs: ['$open', '$disabled', '$clear', '$value', '$highlighted', '$empty'] }
231
- { id: 'multi-select', name: 'MultiSelect', lines: 146, desc: 'Multi-select with chips, filtering, and keyboard nav.', props: ['@value', '@items', '@placeholder', '@disabled'], events: ['change'], keys: ['↕', 'Enter', 'Esc', 'Backspace', 'Tab'], attrs: ['$open', '$disabled', '$chips', '$chip', '$remove', '$clear', '$highlighted', '$selected'] }
232
+ { id: 'multi-select', name: 'MultiSelect', lines: 204, desc: 'Multi-select with chips, filtering, and keyboard nav.', props: ['@value', '@items', '@placeholder', '@disabled'], events: ['change'], keys: ['↕', 'Enter', 'Esc', 'Backspace', 'Tab'], attrs: ['$open', '$disabled', '$chips', '$chip', '$remove', '$highlighted', '$selected'] }
232
233
  { id: 'autocomplete', name: 'Autocomplete', lines: 113, desc: 'Suggestion input — type to filter, select to fill.', props: ['@value', '@items', '@placeholder', '@disabled'], events: ['select'], keys: ['↕', 'Enter', 'Esc', 'Tab'], attrs: ['$open', '$disabled', '$clear'] }
233
234
  { id: 'checkbox', name: 'Checkbox', lines: 18, desc: 'Toggle with checkbox or switch semantics.', props: ['@checked', '@disabled', '@indeterminate', '@switch'], events: ['change'], keys: [], attrs: ['$checked', '$indeterminate', '$disabled'] }
234
235
  { id: 'toggle', name: 'Toggle', lines: 14, desc: 'Two-state toggle button with pressed state.', props: ['@pressed', '@disabled'], events: ['change'], keys: [], attrs: ['$pressed', '$disabled'] }
@@ -646,7 +647,7 @@ export WidgetGallery = component
646
647
  # MULTI-SELECT
647
648
  # ========================================================================
648
649
  .section id: "multi-select"
649
- SectionHead tag: "multi-select", name: "MultiSelect", lines: 146
650
+ SectionHead tag: "multi-select", name: "MultiSelect", lines: 204
650
651
  .section-desc "Multi-select with chips, filtering, and keyboard navigation."
651
652
  .demo-row
652
653
  .demo-label "Pick colors"
@@ -677,7 +678,6 @@ export WidgetGallery = component
677
678
  code "$chips"
678
679
  code "$chip"
679
680
  code "$remove"
680
- code "$clear"
681
681
  code "$highlighted"
682
682
  code "$selected"
683
683
 
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.15.4",
3
+ "version": "3.16.1",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
7
7
  "workspaces": [
8
+ "examples/*",
8
9
  "packages/*"
9
10
  ],
11
+ "catalog": {
12
+ "typescript": "5.9.3"
13
+ },
10
14
  "browser": "docs/dist/rip.min.js",
11
15
  "exports": {
12
16
  ".": {
@@ -35,7 +39,8 @@
35
39
  "bump": "bun scripts/bump.js",
36
40
  "gen:dom": "bun scripts/gen-dom.js",
37
41
  "gallery": "bun scripts/gallery.js",
38
- "bundle:demo": "bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t 'Rip App Demo'",
42
+ "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
+ "bundle:ui": "bun scripts/bundle-app.js packages/ui/browser/components --prefix _pkg/ui -o docs/ui/bundle.json -t 'Rip UI'",
39
44
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
40
45
  "postinstall": "node scripts/postinstall.js --quiet",
41
46
  "link-local": "bun scripts/link-local.js",
@@ -86,6 +91,7 @@
86
91
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
87
92
  "license": "MIT",
88
93
  "devDependencies": {
89
- "typescript": "5.9.3"
94
+ "@types/bun": "1.3.14",
95
+ "typescript": "catalog:"
90
96
  }
91
97
  }
package/rip-loader.js CHANGED
@@ -2,13 +2,74 @@
2
2
 
3
3
  import { plugin } from "bun";
4
4
  import { fileURLToPath } from "url";
5
+ import { dirname, resolve as resolvePath } from "path";
6
+ import { readFileSync, existsSync } from "fs";
5
7
  import { compileToJS, formatError } from "./src/compiler.js";
8
+ // Register the full schema runtime provider so .rip files containing
9
+ // `schema :model` blocks compile correctly inside spawned workers.
10
+ // Workers are launched with `--preload rip-loader.js` and otherwise
11
+ // would call compileToJS without ever registering a provider.
12
+ import "./src/schema/loader-server.js";
13
+
14
+ // ── Undeclared-import diagnostic ────────────────────────────────────────
15
+ // Walk up from an importer to its nearest package.json, then verify that any
16
+ // `@rip-lang/<pkg>` specifier is declared in dependencies/devDependencies/
17
+ // peerDependencies/optionalDependencies (or is the package's own self-import).
18
+ //
19
+ // Throws a clear error before `import.meta.resolve` is even attempted, so
20
+ // "works on my machine" failures rooted in link-global rescue surface loudly
21
+ // instead of silently shipping.
22
+ const declarationCache = new Map(); // importerDir → { pkgName, declared } | null
23
+
24
+ function getDeclarationInfo(importerPath) {
25
+ const start = dirname(importerPath);
26
+ if (declarationCache.has(start)) return declarationCache.get(start);
27
+ let cur = start;
28
+ let info = null;
29
+ while (true) {
30
+ const pkgPath = resolvePath(cur, 'package.json');
31
+ if (existsSync(pkgPath)) {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
34
+ info = {
35
+ pkgName: pkg.name || null,
36
+ declared: new Set([
37
+ ...Object.keys(pkg.dependencies || {}),
38
+ ...Object.keys(pkg.devDependencies || {}),
39
+ ...Object.keys(pkg.peerDependencies || {}),
40
+ ...Object.keys(pkg.optionalDependencies || {}),
41
+ ]),
42
+ };
43
+ } catch {
44
+ info = { pkgName: null, declared: new Set() };
45
+ }
46
+ break;
47
+ }
48
+ const parent = dirname(cur);
49
+ if (parent === cur) break;
50
+ cur = parent;
51
+ }
52
+ declarationCache.set(start, info);
53
+ return info;
54
+ }
55
+
56
+ export function assertDeclaredRipImport(importerPath, specifier) {
57
+ const m = specifier.match(/^(@rip-lang\/[^\/]+)/);
58
+ if (!m) return;
59
+ const pkgKey = m[1];
60
+ const info = getDeclarationInfo(importerPath);
61
+ if (!info) return; // ad-hoc script outside any package — don't block
62
+ if (info.pkgName === pkgKey) return; // self-import
63
+ if (info.declared.has(pkgKey)) return;
64
+ throw new Error(
65
+ `Import of '${pkgKey}' is not declared in package.json. ` +
66
+ `Run \`bun add ${pkgKey}\` (or use \`workspace:*\` inside this monorepo).`
67
+ );
68
+ }
6
69
 
7
70
  await plugin({
8
71
  name: "rip-loader",
9
72
  async setup(build) {
10
- const { readFileSync } = await import("fs");
11
-
12
73
  // Handle .rip files
13
74
  build.onLoad({ filter: /\.rip$/ }, async (args) => {
14
75
  try {
@@ -20,6 +81,7 @@ await plugin({
20
81
  // is broken in plugin handlers — so we use import.meta.resolve, which
21
82
  // resolves from this file's location (inside the global node_modules tree).
22
83
  js = js.replace(/(from\s+|import\s*\()(['"])(@rip-lang\/[^'"]+)\2/g, (match, prefix, quote, specifier) => {
84
+ assertDeclaredRipImport(args.path, specifier);
23
85
  try {
24
86
  return `${prefix}${quote}${fileURLToPath(import.meta.resolve(specifier))}${quote}`;
25
87
  } catch {
package/src/AGENTS.md CHANGED
@@ -1,21 +1,24 @@
1
1
  # Compiler Subsystem — Agent Guide
2
2
 
3
- This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `types.js`, `types-emit.js`, `app.rip`, `typecheck.js`, the `schema/` subdirectory, and the `grammar/` directory. The schema feature lives in `src/schema/` (entry `src/schema/schema.js`, imported via relative paths like `./schema/schema.js` from sibling modules).
3
+ This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `stdlib.js`, `types.js`, `dts.js`, `typecheck.js`, the `schema/` subdirectory, and the `grammar/` directory. The schema feature lives in `src/schema/` (entry `src/schema/schema.js`, imported via relative paths like `./schema/schema.js` from sibling modules).
4
+
5
+ The Rip App framework — stash, resource, timing, components store, router, renderer, launch, ARIA helpers — used to live alongside the compiler at `src/app.rip`. It now lives at `packages/app/index.rip` (peer of `packages/server/`, `packages/ui/`, etc.). The browser bundle still includes it via `scripts/build.js`; the source-tree split just reflects that it's user-land Rip code, not a compiler internal.
4
6
 
5
7
  ---
6
8
 
7
9
  ## Module Map — browser-side vs CLI-only
8
10
 
9
- The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus the compiled `src/app.rip`. Every module statically reachable from either entry ends up in the bundle. `scripts/check-bundle-graph.js` walks both entries on every `bun run build` and fails if any reachable file matches a forbidden list.
11
+ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus the compiled `packages/app/index.rip`. Every module statically reachable from either entry ends up in the bundle. `scripts/check-bundle-graph.js` walks both entries on every `bun run build` and fails if any reachable file matches a forbidden list.
10
12
 
11
13
  | Module | Browser? | Purpose |
12
14
  | --- | --- | --- |
13
15
  | `src/browser.js` | yes (entry) | `<script type="text/rip">` discovery, `processRipScripts`, `importRip`, REPL |
14
- | `src/app.rip` | yes (entry) | Rip App framework runtime: stash, resource, timing, components store, router, renderer, launch, ARIA helpers |
16
+ | `packages/app/index.rip` | yes (entry) | Rip App framework runtime: stash, resource, timing, components store, router, renderer, launch, ARIA helpers |
15
17
  | `src/parser.js` | yes | generated LR table |
16
18
  | `src/lexer.js` | yes | tokenizer + rewriter pipeline |
17
19
  | `src/compiler.js` | yes | codegen + reactive runtime + component runtime + `compileToJS` + `setTypesEmitter` hook + `emitEnum` |
18
20
  | `src/components.js` | yes | render rewriter + component runtime |
21
+ | `src/stdlib.js` | yes | `getStdlibCode()` runtime preamble (p / pp / pr / pj / kind / abort / ...) + importable `stringify(value)` + `STDLIB_TYPE_DECLS` for typecheck.js |
19
22
  | `src/schema/schema.js` | yes (via `./schema/schema.js`) | lexer rewrite + body parser + emitSchema codegen + `setSchemaRuntimeProvider` hook (no fragment imports) |
20
23
  | `src/schema/loader-browser.js` | yes (browser only, via `./schema/loader-browser.js`) | imports validate + browser-stubs fragments; eager-installs browser runtime; registers provider |
21
24
  | `src/schema/loader-server.js` | **no** (CLI / server / tests, via `./schema/loader-server.js`) | imports all five fragments; eager-installs migration runtime; registers provider |
@@ -30,8 +33,8 @@ The browser bundle (`docs/dist/rip.min.js`) is built from `src/browser.js` plus
30
33
  | `src/sourcemaps.js` | yes | inline source-map generation |
31
34
  | `src/generated/dom-tags.js` | yes | HTML/SVG tag set for render-block tag detection |
32
35
  | `src/generated/dom-events.js` | yes | event-name set for `onClick`/`onKeydown` auto-wire |
33
- | `src/types-emit.js` | **no** | `.d.ts` emitter + intrinsic decl tables — CLI / typecheck only |
34
- | `src/schema/dts-emit.js` | **no** | schema `.d.ts` emitter — CLI / typecheck only |
36
+ | `src/dts.js` | **no** | `.d.ts` emitter + intrinsic decl tables for the type system — CLI / typecheck only |
37
+ | `src/schema/dts.js` | **no** | `.d.ts` emitter for schema declarations — CLI / typecheck only |
35
38
  | `src/typecheck.js` | **no** | TypeScript LSP integration — CLI only |
36
39
  | `src/repl.js` | **no** | interactive CLI REPL |
37
40
 
@@ -43,22 +46,22 @@ The same pattern is used twice — once for `.d.ts` emission, once for the schem
43
46
 
44
47
  `compiler.js` exports `setTypesEmitter(fn)`. The default emitter is `null`. The two `compile()` callsites that produce `.d.ts` output guard with `(typeTokens && _typesEmitter)` and silently skip if no emitter is registered.
45
48
 
46
- `src/types-emit.js` calls `setTypesEmitter(emitTypes)` at module load. Any caller that wants `.d.ts` output side-effect-imports `types-emit.js`:
49
+ `src/dts.js` calls `setTypesEmitter(emitTypes)` at module load. Any caller that wants `.d.ts` output side-effect-imports `dts.js`:
47
50
 
48
51
  ```javascript
49
52
  // CLI entry — bin/rip
50
- import '../src/types-emit.js'; // installs emitter
53
+ import '../src/dts.js'; // installs emitter
51
54
 
52
55
  // LSP integration
53
- import { ... } from './types-emit.js';
56
+ import { ... } from './dts.js';
54
57
 
55
58
  // Test runner that exercises type emission
56
- import '../src/types-emit.js';
59
+ import '../src/dts.js';
57
60
  ```
58
61
 
59
- The browser bundle never imports `types-emit.js`, so the emitter stays null and the `.d.ts` path is dead code that the bundler prunes.
62
+ The browser bundle never imports `dts.js`, so the emitter stays null and the `.d.ts` path is dead code that the bundler prunes.
60
63
 
61
- **Failure mode to remember:** If you write code that calls `compile(source, { types: 'emit' })` and inspects `result.dts`, you **must** import `src/types-emit.js` (directly or indirectly) somewhere in that code path. Without it, `result.dts` is `null` regardless of source content. Symptom: types emission "silently does nothing" — no error, no warning, just empty output. The fix is one line: `import '../src/types-emit.js';`.
64
+ **Failure mode to remember:** If you write code that calls `compile(source, { types: 'emit' })` and inspects `result.dts`, you **must** import `src/dts.js` (directly or indirectly) somewhere in that code path. Without it, `result.dts` is `null` regardless of source content. Symptom: types emission "silently does nothing" — no error, no warning, just empty output. The fix is one line: `import '../src/dts.js';`.
62
65
 
63
66
  The schema runtime uses an analogous hook: `src/schema/schema.js` exports `setSchemaRuntimeProvider(fn)`, default null. `src/schema/loader-server.js` and `src/schema/loader-browser.js` are the two providers. CLI / tests / server side-effect-import `./schema/loader-server.js` (full migration runtime, all four modes). The browser bundle (`src/browser.js`) side-effect-imports `./schema/loader-browser.js` (validate + browser-stubs only). Same failure mode applies — call `getSchemaRuntime()` without registering a provider and you get a clear error pointing at which loader to import.
64
67
 
@@ -185,6 +188,8 @@ Complete node reference:
185
188
  ['++', expr, isPostfix] ['--', expr, isPostfix]
186
189
 
187
190
  // Control Flow
191
+ // `elseBlock` may itself be another `['if', ...]` to form an `else if` chain
192
+ // (right-recursive nesting), or any other branch shape for a terminal else.
188
193
  ['if', condition, thenBlock, elseBlock?]
189
194
  ['unless', condition, body]
190
195
  ['?:', condition, thenExpr, elseExpr]
@@ -236,15 +241,15 @@ Complete node reference:
236
241
  Tokens are `[tag, val]` arrays with extra properties:
237
242
 
238
243
  - `.pre` — whitespace count before token
239
- - `.data` — metadata like `{ await, predicate, quote, invert, parsedValue }`
244
+ - `.data` — metadata like `{ await, optional, quote, invert, parsedValue }`
240
245
  - `.loc` — `{ r, c, n }`
241
246
  - `.spaced` — sugar for `.pre > 0`
242
247
  - `.newLine` — whether preceded by newline
243
248
 
244
249
  Identifier suffixes:
245
250
 
246
- - `!` sets `.data.await = true`
247
- - `?` sets `.data.predicate = true`
251
+ - `!` sets `.data.bang = true` — a neutral "trailing `!`" flag resolved by context downstream: dammit/`await` at a call site (`fetch!` → `await fetch()`), or the void marker at a function definition (`foo! = ->`, `def foo!` → no implicit return). Void-ness is stamped onto the function node as `isVoid` by `applyVoidMarker` and read locally by the arrow emitters; `def` reads `meta(name, 'bang')` directly.
252
+ - `?` sets `.data.optional = true` (existence check on values; optional marker on prop/type-field names)
248
253
  - `as!` in loops emits `FORASAWAIT` for `for await`
249
254
 
250
255
  Tagged template bridge:
@@ -410,7 +415,7 @@ Block factories need locals and `ctx.member` references instead of `this._elN` a
410
415
  - `_factoryVars` — variables that need local `let` declarations
411
416
  - `_fragChildren` — fragment-to-children tracking for removals
412
417
  - `_pushEffect(body)` — emits `__effect(...)` or `disposers.push(__effect(...))`
413
- - `_loopVarStack` — threads loop variables through nested factories
418
+ - `_loopVarStack` — threads loop variables through nested factories. Each frame is `{ itemVar, indexVar, reactiveSource }`; `reactiveSource` is computed once when the loop is emitted (via `hasReactiveDeps(collection)`) and tells `hasReactiveDeps` to treat direct member access rooted at `itemVar`/`indexVar` (`item.foo`, `item[0]`, `item.a.b`) as reactive. Alias and destructuring forms are not tracked.
414
419
 
415
420
  Factory mode is entered in `emitConditionBranch` and `emitTemplateLoop` via save/restore of `[_createLines, _setupLines, _factoryMode, _factoryVars]`.
416
421
 
@@ -425,6 +430,13 @@ Methods named `onClick`, `onKeydown`, `onMouseenter`, etc. automatically bind to
425
430
  - lifecycle exclusion: `onError` is not auto-wired
426
431
  - after bumping `typescript`, refresh the generated DOM metadata with `bun run gen:dom`
427
432
 
433
+ ### Inherited Props (`extends <tag>`)
434
+
435
+ User-facing docs: [docs/RIP-LANG.md](../docs/RIP-LANG.md) → "Inherited Props".
436
+ Runtime entry points to grep when working on this: `_bindInheritedTarget`,
437
+ `_setRestProp`, `_applyRestToInheritedEl`, `_inheritsTag`. Tests:
438
+ [test/rip/components.rip](../test/rip/components.rip) under "extends ...".
439
+
428
440
  ### Generated DOM Tag Sets
429
441
 
430
442
  - `src/generated/dom-tags.js` is generated from TypeScript's `HTMLElementTagNameMap` and `SVGElementTagNameMap`
@@ -461,30 +473,35 @@ Compile-time optimizations:
461
473
  - array-based `state.blocks[]`
462
474
  - `state.keys = items.slice()` for default item-as-key behavior
463
475
 
464
- ### Nested Loop Variable Collision (known gotcha)
476
+ ### Nested Loop Variable Collision (fixed)
465
477
 
466
- The emitted patch function for a reactive block is named `p` and takes
467
- every enclosing loop variable as a positional parameter:
478
+ Historically, `emitTemplateLoop` would auto-allocate `i` for any
479
+ outer loop with no explicit index. An inner `for v, i in ...` then
480
+ explicitly bound `i`, and both ended up as positional parameters of
481
+ the deepest factory's patch function:
468
482
 
469
483
  ```javascript
470
- // For a render with `for item in items` containing `for v, i in item.enum`
471
- p(ctx, v, i, item, i) { ... } // duplicate `i` — invalid in strict mode
484
+ // Pre-fix output:
485
+ p(ctx, v, i, item, i) { ... } // duplicate `i` — strict-mode SyntaxError
472
486
  ```
473
487
 
474
- The outer `for item in items` allocates an implicit `i` counter even
475
- when the user wrote no explicit index. If the inner loop uses `i` as an
476
- explicit index, both end up in `p`'s signature and V8 throws
477
- `Duplicate parameter name not allowed in this context` at parse time.
488
+ The fix in `emitTemplateLoop` (`src/components.js`) calls
489
+ `_collectExplicitLoopIdentifiers(body, usedNames)` to pre-scan the
490
+ entire loop body for every explicit `for-in`/`for-of`/`for-as` var,
491
+ then picks the first conventional letter (`i,j,k,l,m,n`) that's not
492
+ in `usedNames`. If every letter collides, it falls back to
493
+ `__rip_idx${depth}` — a name no normal user identifier writes.
478
494
 
479
- Current workaround (author-facing, documented in
480
- `packages/ui/AGENTS.md`): use a different inner index name (`idx`, `n`,
481
- `j`).
495
+ The same nested case now compiles cleanly:
496
+
497
+ ```javascript
498
+ function create_block_outer(ctx, item, j) { ... }
499
+ function create_block_inner(ctx, v, i, item, j) { ... } // unique
500
+ ```
482
501
 
483
- Long-term fix: the emitter should generate unique internal names for
484
- auto-allocated loop counters (e.g. `__i0`, `__i1`) rather than reusing
485
- `i`, so no user-chosen name can ever collide. The fix lives in whichever
486
- `emitFor*` path closes over the block into a patch function — search
487
- for sites that build the `p(ctx, ...args)` signature in `compiler.js`.
502
+ User-explicit collisions (`for x, i in xs / for y, i in ys`) are
503
+ still a real strict-mode error and are intentionally left to the
504
+ user the compiler doesn't silently rewrite the names you typed.
488
505
 
489
506
  ### Error Boundaries
490
507
 
@@ -556,6 +573,16 @@ The browser bundle is an IIFE loaded with `<script defer>`, not `type="module"`,
556
573
 
557
574
  `let` declarations are stripped so values can persist in sloppy-mode eval, while `const` is hoisted to `globalThis`.
558
575
 
576
+ ### `window.__RIP__` — same surface on both compile paths (don't break this)
577
+
578
+ `processRipScripts()` has two compile paths: with `data-router` it calls `app.launch()`; without `data-router` it inlines the bundles into one async-IIFE eval. Both paths expose the **same** debug surface on `window.__RIP__`, and consumers (e.g. the docs UI gallery's view-source feature) read from it indiscriminately:
579
+
580
+ - `window.__RIP__.components.read("components/<id>.rip")` — returns the bundled `.rip` source text.
581
+
582
+ `launch()` already wires this up (see `app.rip` ~1190). The no-router path used to silently skip it, so `window.__RIP__` was undefined for any deploy that used `data-src="bundle.json"` alone — view-source UIs would silently fail. The fix mirrors `launch()`'s setup: build a `createComponents()` store from the bundles and expose it on `window.__RIP__.components` (see `browser.js` ~226–239).
583
+
584
+ **Invariant:** any future refactor of either compile path must keep `window.__RIP__.components.read(path)` working after boot. The regression test that locks this in lives in `test/bundle.test.js` (boot-simulation driver — fakes a `<script src="rip.min.js" data-src="bundle.json">` runtime tag and a synthetic bundle, awaits `globalThis.__ripScriptsReady`, asserts `read()` returns the source). If you delete or move that block in `browser.js`, that test will fail with `window.__RIP__ missing — no-router path did not wire components store`.
585
+
559
586
  ---
560
587
 
561
588
  ## Reactivity Implementation
@@ -626,11 +653,11 @@ enum Status
626
653
  Type emission is split across two files by execution context:
627
654
 
628
655
  - `types.js` (browser-side, ~21 KB) — `installTypeSupport(Lexer)` adds `rewriteTypes()` to strip type annotations from the token stream so user-typed Rip parses. This is the only thing the browser needs from type machinery.
629
- - `types-emit.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `expandSuffixes`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
656
+ - `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
630
657
 
631
658
  `emitEnum` (runtime JS for `enum` blocks) lives in `compiler.js` next to the rest of the codegen dispatch — it's not type machinery, it's real runtime emission.
632
659
 
633
- `typecheck.js` (CLI only) drives `rip check`, mediates TypeScript diagnostics, and side-effect-imports `types-emit.js` for the intrinsic decl tables.
660
+ `typecheck.js` (CLI only) drives `rip check`, mediates TypeScript diagnostics, and side-effect-imports `dts.js` for the intrinsic decl tables.
634
661
 
635
662
  Types are processed at the token layer before parsing.
636
663
 
@@ -664,10 +691,10 @@ several files by execution context:
664
691
  bundle. Server loader pulls all five; browser loader pulls only
665
692
  validate + browser-stubs. Bun's tree-shaker uses these import sets to
666
693
  omit server-only fragments from `docs/dist/rip.min.js`.
667
- - `src/schema/dts-emit.js` (CLI/LSP only) — `emitSchemaTypes` walks
694
+ - `src/schema/dts.js` (CLI/LSP only) — `emitSchemaTypes` walks
668
695
  parsed schema s-expressions and emits `declare const Foo: Schema<...>`
669
696
  lines for the TypeScript language service. Imported only by
670
- `types-emit.js` and `typecheck.js`. The `dts-emit` name signals
697
+ `dts.js` and `typecheck.js`. The `dts` name signals
671
698
  that this is a compile-time `.d.ts` emitter, not a `runtime-*` fragment.
672
699
 
673
700
  ### Lexer path