rip-lang 3.14.4 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -1,14 +1,14 @@
1
1
  {
2
- "css": "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');\n\n/**\n * Rip UI Demo — Self-contained styles\n * No external dependencies (no Tailwind)\n */\n\n/* ============================================\n Design Tokens\n ============================================ */\n\n:root {\n --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n\n --gray-50: #f9fafb;\n --gray-100: #f3f4f6;\n --gray-200: #e5e7eb;\n --gray-300: #d1d5db;\n --gray-400: #9ca3af;\n --gray-500: #6b7280;\n --gray-600: #4b5563;\n --gray-700: #374151;\n --gray-800: #1f2937;\n --gray-900: #111827;\n\n --blue-50: #eff6ff;\n --blue-100: #dbeafe;\n --blue-500: #3b82f6;\n --blue-600: #2563eb;\n --blue-700: #1d4ed8;\n\n --green-50: #f0fdf4;\n --green-500: #22c55e;\n --green-600: #16a34a;\n\n --red-50: #fef2f2;\n --red-500: #ef4444;\n\n --surface: var(--gray-50);\n --card: #ffffff;\n --text: var(--gray-900);\n --text-secondary: var(--gray-500);\n --text-muted: var(--gray-400);\n --border: var(--gray-200);\n --accent: var(--blue-600);\n --accent-hover: var(--blue-700);\n\n --radius: 0.75rem;\n --radius-lg: 1rem;\n --shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px rgb(0 0 0 / 0.06);\n --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* ============================================\n Reset\n ============================================ */\n\n*, *::before, *::after { box-sizing: border-box; }\n* { margin: 0; }\n\nhtml { -webkit-text-size-adjust: 100%; }\n\nbody {\n font-family: var(--font);\n line-height: 1.6;\n color: var(--text);\n background: var(--surface);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\na { color: inherit; text-decoration: none; }\nimg, svg { display: block; max-width: 100%; }\ninput, button, textarea, select { font: inherit; }\n\n#app {\n max-width: 52rem;\n margin: 0 auto;\n padding: 2rem;\n}\n\n/* ============================================\n Layout Shell\n ============================================ */\n\n.app-layout {\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\n\n.app-nav {\n background: var(--card);\n border-bottom: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 0 1.5rem;\n position: sticky;\n top: 0;\n z-index: 100;\n}\n\n.nav-inner {\n max-width: 48rem;\n margin: 0 auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 3.5rem;\n}\n\n.nav-brand {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-weight: 600;\n font-size: 0.9375rem;\n letter-spacing: -0.02em;\n}\n\n.nav-links {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.nav-link {\n padding: 0.375rem 0.75rem;\n border-radius: 0.5rem;\n color: var(--text-secondary);\n font-size: 0.8125rem;\n font-weight: 500;\n transition: color var(--transition), background-color var(--transition);\n}\n\n.nav-link:hover {\n color: var(--text);\n background-color: var(--gray-100);\n}\n\n.nav-link.active {\n color: var(--accent);\n background: var(--blue-50);\n}\n\n.app-main {\n flex: 1;\n max-width: 48rem;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n}\n\n/* ============================================\n Page Sections\n ============================================ */\n\n.page-header {\n margin-bottom: 2rem;\n}\n\n.page-title {\n font-size: 1.5rem;\n font-weight: 700;\n letter-spacing: -0.03em;\n line-height: 1.2;\n color: var(--text);\n}\n\n.page-subtitle {\n font-size: 0.875rem;\n color: var(--text-secondary);\n margin-top: 0.25rem;\n}\n\n/* ============================================\n Card\n ============================================ */\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n padding: 1.5rem;\n}\n\n.card + .card {\n margin-top: 1rem;\n}\n\n.card-title {\n font-size: 0.8125rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-secondary);\n margin-bottom: 1rem;\n}\n\n/* ============================================\n Button\n ============================================ */\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background-color: var(--card);\n color: var(--text);\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n transition: color var(--transition), background-color var(--transition), border-color var(--transition), transform var(--transition);\n user-select: none;\n}\n\n.btn:hover { background-color: var(--gray-50); border-color: var(--gray-300); }\n.btn:active { transform: scale(0.98); }\n\n.btn-primary {\n background: var(--accent);\n border-color: var(--accent);\n color: white;\n}\n\n.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }\n\n.btn-danger {\n color: var(--red-500);\n border-color: var(--red-500);\n}\n\n.btn-danger:hover { background: var(--red-50); }\n\n.btn-sm {\n padding: 0.25rem 0.5rem;\n font-size: 0.75rem;\n}\n\n.btn-group {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n\n/* ============================================\n Counter Display\n ============================================ */\n\n.counter-display {\n font-size: 4rem;\n font-weight: 700;\n letter-spacing: -0.04em;\n text-align: center;\n padding: 2rem 0;\n font-variant-numeric: tabular-nums;\n color: var(--text);\n transition: color var(--transition);\n}\n\n.counter-display.positive { color: var(--green-600); }\n.counter-display.negative { color: var(--red-500); }\n\n.counter-controls {\n display: flex;\n justify-content: center;\n gap: 0.5rem;\n}\n\n/* ============================================\n Input\n ============================================ */\n\n.input {\n width: 100%;\n padding: 0.5rem 0.75rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background-color: var(--card);\n color: var(--text);\n font-size: 0.875rem;\n transition: border-color var(--transition), box-shadow var(--transition);\n}\n\n.input::placeholder { color: var(--text-muted); }\n.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--blue-100); }\n\n/* ============================================\n Todo List\n ============================================ */\n\n.todo-input-row {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1rem;\n}\n\n.todo-input-row .input { flex: 1; }\n\n.todo-list {\n list-style: none;\n padding: 0;\n}\n\n.todo-item {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--gray-100);\n transition: opacity var(--transition);\n}\n\n.todo-item:last-child { border-bottom: none; }\n\n.todo-checkbox {\n width: 1.25rem;\n height: 1.25rem;\n border: 2px solid var(--gray-300);\n border-radius: 0.375rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background-color var(--transition), border-color var(--transition);\n flex-shrink: 0;\n}\n\n.todo-checkbox:hover { border-color: var(--accent); }\n\n.todo-checkbox.checked {\n background: var(--accent);\n border-color: var(--accent);\n}\n\n.todo-checkbox.checked::after {\n content: '';\n width: 0.4rem;\n height: 0.25rem;\n border-left: 2px solid white;\n border-bottom: 2px solid white;\n transform: rotate(-45deg);\n margin-bottom: 1px;\n}\n\n.todo-text {\n flex: 1;\n font-size: 0.875rem;\n color: var(--text);\n}\n\n.todo-text.done {\n text-decoration: line-through;\n color: var(--text-muted);\n}\n\n.todo-delete {\n opacity: 0;\n padding: 0.125rem 0.375rem;\n border: none;\n background: none;\n color: var(--text-muted);\n cursor: pointer;\n border-radius: 0.25rem;\n font-size: 0.75rem;\n transition: opacity var(--transition), color var(--transition), background-color var(--transition);\n}\n\n.todo-item:hover .todo-delete { opacity: 1; }\n.todo-delete:hover { color: var(--red-500); background: var(--red-50); }\n\n.todo-stats {\n display: flex;\n justify-content: space-between;\n padding-top: 0.75rem;\n border-top: 1px solid var(--gray-100);\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n/* ============================================\n Feature Grid (Home page)\n ============================================ */\n\n.feature-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 1rem;\n}\n\n@media (max-width: 640px) {\n .feature-grid { grid-template-columns: 1fr; }\n}\n\n.feature-card {\n background-color: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n transition: border-color var(--transition), box-shadow var(--transition);\n}\n\n.feature-card:hover {\n border-color: var(--gray-300);\n box-shadow: var(--shadow);\n}\n\n.feature-icon {\n font-size: 1.5rem;\n margin-bottom: 0.75rem;\n}\n\n.feature-name {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--text);\n margin-bottom: 0.25rem;\n}\n\n.feature-desc {\n font-size: 0.8125rem;\n color: var(--text-secondary);\n line-height: 1.5;\n}\n\n/* ============================================\n Code Block\n ============================================ */\n\n.code-block {\n background: var(--gray-800);\n color: var(--gray-100);\n padding: 1rem 1.25rem;\n border-radius: var(--radius);\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 0.8125rem;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n}\n\n.code-keyword { color: #c084fc; }\n.code-string { color: #86efac; }\n.code-comment { color: var(--gray-500); }\n.code-number { color: #fbbf24; }\n\n/* ============================================\n Stat badge\n ============================================ */\n\n.stat {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.75rem;\n background: var(--gray-100);\n border-radius: var(--radius);\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--text-secondary);\n}\n\n.stat-value {\n font-weight: 700;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n}",
2
+ "css": "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');\n\n/**\n * Rip App Demo — Self-contained styles\n * No external dependencies (no Tailwind)\n */\n\n/* ============================================\n Design Tokens\n ============================================ */\n\n:root {\n --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n\n --gray-50: #f9fafb;\n --gray-100: #f3f4f6;\n --gray-200: #e5e7eb;\n --gray-300: #d1d5db;\n --gray-400: #9ca3af;\n --gray-500: #6b7280;\n --gray-600: #4b5563;\n --gray-700: #374151;\n --gray-800: #1f2937;\n --gray-900: #111827;\n\n --blue-50: #eff6ff;\n --blue-100: #dbeafe;\n --blue-500: #3b82f6;\n --blue-600: #2563eb;\n --blue-700: #1d4ed8;\n\n --green-50: #f0fdf4;\n --green-500: #22c55e;\n --green-600: #16a34a;\n\n --red-50: #fef2f2;\n --red-500: #ef4444;\n\n --surface: var(--gray-50);\n --card: #ffffff;\n --text: var(--gray-900);\n --text-secondary: var(--gray-500);\n --text-muted: var(--gray-400);\n --border: var(--gray-200);\n --accent: var(--blue-600);\n --accent-hover: var(--blue-700);\n\n --radius: 0.75rem;\n --radius-lg: 1rem;\n --shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 1px 2px rgb(0 0 0 / 0.06);\n --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);\n --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* ============================================\n Reset\n ============================================ */\n\n*, *::before, *::after { box-sizing: border-box; }\n* { margin: 0; }\n\nhtml { -webkit-text-size-adjust: 100%; }\n\nbody {\n font-family: var(--font);\n line-height: 1.6;\n color: var(--text);\n background: var(--surface);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\na { color: inherit; text-decoration: none; }\nimg, svg { display: block; max-width: 100%; }\ninput, button, textarea, select { font: inherit; }\n\n#app {\n max-width: 52rem;\n margin: 0 auto;\n padding: 2rem;\n}\n\n/* ============================================\n Layout Shell\n ============================================ */\n\n.app-layout {\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\n\n.app-nav {\n background: var(--card);\n border-bottom: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 0 1.5rem;\n position: sticky;\n top: 0;\n z-index: 100;\n}\n\n.nav-inner {\n max-width: 48rem;\n margin: 0 auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 3.5rem;\n}\n\n.nav-brand {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n font-weight: 600;\n font-size: 0.9375rem;\n letter-spacing: -0.02em;\n}\n\n.nav-links {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.nav-link {\n padding: 0.375rem 0.75rem;\n border-radius: 0.5rem;\n color: var(--text-secondary);\n font-size: 0.8125rem;\n font-weight: 500;\n transition: color var(--transition), background-color var(--transition);\n}\n\n.nav-link:hover {\n color: var(--text);\n background-color: var(--gray-100);\n}\n\n.nav-link.active {\n color: var(--accent);\n background: var(--blue-50);\n}\n\n.app-main {\n flex: 1;\n max-width: 48rem;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n}\n\n/* ============================================\n Page Sections\n ============================================ */\n\n.page-header {\n margin-bottom: 2rem;\n}\n\n.page-title {\n font-size: 1.5rem;\n font-weight: 700;\n letter-spacing: -0.03em;\n line-height: 1.2;\n color: var(--text);\n}\n\n.page-subtitle {\n font-size: 0.875rem;\n color: var(--text-secondary);\n margin-top: 0.25rem;\n}\n\n/* ============================================\n Card\n ============================================ */\n\n.card {\n background: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n padding: 1.5rem;\n}\n\n.card + .card {\n margin-top: 1rem;\n}\n\n.card-title {\n font-size: 0.8125rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-secondary);\n margin-bottom: 1rem;\n}\n\n/* ============================================\n Button\n ============================================ */\n\n.btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n padding: 0.5rem 1rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background-color: var(--card);\n color: var(--text);\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n transition: color var(--transition), background-color var(--transition), border-color var(--transition), transform var(--transition);\n user-select: none;\n}\n\n.btn:hover { background-color: var(--gray-50); border-color: var(--gray-300); }\n.btn:active { transform: scale(0.98); }\n\n.btn-primary {\n background: var(--accent);\n border-color: var(--accent);\n color: white;\n}\n\n.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }\n\n.btn-danger {\n color: var(--red-500);\n border-color: var(--red-500);\n}\n\n.btn-danger:hover { background: var(--red-50); }\n\n.btn-sm {\n padding: 0.25rem 0.5rem;\n font-size: 0.75rem;\n}\n\n.btn-group {\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n\n/* ============================================\n Counter Display\n ============================================ */\n\n.counter-display {\n font-size: 4rem;\n font-weight: 700;\n letter-spacing: -0.04em;\n text-align: center;\n padding: 2rem 0;\n font-variant-numeric: tabular-nums;\n color: var(--text);\n transition: color var(--transition);\n}\n\n.counter-display.positive { color: var(--green-600); }\n.counter-display.negative { color: var(--red-500); }\n\n.counter-controls {\n display: flex;\n justify-content: center;\n gap: 0.5rem;\n}\n\n/* ============================================\n Input\n ============================================ */\n\n.input {\n width: 100%;\n padding: 0.5rem 0.75rem;\n border: 1px solid var(--border);\n border-radius: 0.5rem;\n background-color: var(--card);\n color: var(--text);\n font-size: 0.875rem;\n transition: border-color var(--transition), box-shadow var(--transition);\n}\n\n.input::placeholder { color: var(--text-muted); }\n.input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--blue-100); }\n\n/* ============================================\n Todo List\n ============================================ */\n\n.todo-input-row {\n display: flex;\n gap: 0.5rem;\n margin-bottom: 1rem;\n}\n\n.todo-input-row .input { flex: 1; }\n\n.todo-list {\n list-style: none;\n padding: 0;\n}\n\n.todo-item {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.75rem 0;\n border-bottom: 1px solid var(--gray-100);\n transition: opacity var(--transition);\n}\n\n.todo-item:last-child { border-bottom: none; }\n\n.todo-checkbox {\n width: 1.25rem;\n height: 1.25rem;\n border: 2px solid var(--gray-300);\n border-radius: 0.375rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background-color var(--transition), border-color var(--transition);\n flex-shrink: 0;\n}\n\n.todo-checkbox:hover { border-color: var(--accent); }\n\n.todo-checkbox.checked {\n background: var(--accent);\n border-color: var(--accent);\n}\n\n.todo-checkbox.checked::after {\n content: '';\n width: 0.4rem;\n height: 0.25rem;\n border-left: 2px solid white;\n border-bottom: 2px solid white;\n transform: rotate(-45deg);\n margin-bottom: 1px;\n}\n\n.todo-text {\n flex: 1;\n font-size: 0.875rem;\n color: var(--text);\n}\n\n.todo-text.done {\n text-decoration: line-through;\n color: var(--text-muted);\n}\n\n.todo-delete {\n opacity: 0;\n padding: 0.125rem 0.375rem;\n border: none;\n background: none;\n color: var(--text-muted);\n cursor: pointer;\n border-radius: 0.25rem;\n font-size: 0.75rem;\n transition: opacity var(--transition), color var(--transition), background-color var(--transition);\n}\n\n.todo-item:hover .todo-delete { opacity: 1; }\n.todo-delete:hover { color: var(--red-500); background: var(--red-50); }\n\n.todo-stats {\n display: flex;\n justify-content: space-between;\n padding-top: 0.75rem;\n border-top: 1px solid var(--gray-100);\n font-size: 0.75rem;\n color: var(--text-secondary);\n}\n\n/* ============================================\n Feature Grid (Home page)\n ============================================ */\n\n.feature-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 1rem;\n}\n\n@media (max-width: 640px) {\n .feature-grid { grid-template-columns: 1fr; }\n}\n\n.feature-card {\n background-color: var(--card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n padding: 1.25rem;\n transition: border-color var(--transition), box-shadow var(--transition);\n}\n\n.feature-card:hover {\n border-color: var(--gray-300);\n box-shadow: var(--shadow);\n}\n\n.feature-icon {\n font-size: 1.5rem;\n margin-bottom: 0.75rem;\n}\n\n.feature-name {\n font-size: 0.875rem;\n font-weight: 600;\n color: var(--text);\n margin-bottom: 0.25rem;\n}\n\n.feature-desc {\n font-size: 0.8125rem;\n color: var(--text-secondary);\n line-height: 1.5;\n}\n\n/* ============================================\n Code Block\n ============================================ */\n\n.code-block {\n background: var(--gray-800);\n color: var(--gray-100);\n padding: 1rem 1.25rem;\n border-radius: var(--radius);\n font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;\n font-size: 0.8125rem;\n line-height: 1.6;\n overflow-x: auto;\n white-space: pre;\n}\n\n.code-keyword { color: #c084fc; }\n.code-string { color: #86efac; }\n.code-comment { color: var(--gray-500); }\n.code-number { color: #fbbf24; }\n\n/* ============================================\n Stat badge\n ============================================ */\n\n.stat {\n display: inline-flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.375rem 0.75rem;\n background: var(--gray-100);\n border-radius: var(--radius);\n font-size: 0.8125rem;\n font-weight: 500;\n color: var(--text-secondary);\n}\n\n.stat-value {\n font-weight: 700;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n}",
3
3
  "components": {
4
4
  "components/counter.rip": "# Counter Page\n\nexport Counter = component\n count := @app.data.count or 0\n step := 1\n\n ~> @app.data.count = count\n\n increment: -> count += step\n decrement: -> count -= step\n reset: -> count = 0\n\n render\n div\n .page-header\n h1.page-title \"Counter\"\n p.page-subtitle \"Reactive state — persists across reload\"\n\n .card\n .counter-display \"#{count}\"\n\n .counter-controls\n button.btn @click: @decrement, \"- #{step}\"\n button.btn @click: @reset, \"Reset\"\n button.btn.btn-primary @click: @increment, \"+ #{step}\"\n\n .card\n .card-title \"Step Size\"\n .btn-group\n button.btn @click: (-> step = 1), \"1\"\n button.btn @click: (-> step = 5), \"5\"\n button.btn @click: (-> step = 10), \"10\"\n button.btn @click: (-> step = 100), \"100\"\n",
5
- "components/index.rip": "# Home Page\n\nexport Home = component\n\n render\n div\n .page-header\n h1.page-title \"Rip UI Framework\"\n p.page-subtitle \"Zero-build reactive web apps\"\n\n .feature-grid\n .feature-card\n .feature-icon \"🗂\"\n .feature-name \"Component Store\"\n .feature-desc \"Component source files loaded on demand. Compiled in the browser.\"\n\n .feature-card\n .feature-icon \"🔀\"\n .feature-name \"File-Based Router\"\n .feature-desc \"URLs map to components. Dynamic segments and nested layouts.\"\n\n .feature-card\n .feature-icon \"⚡\"\n .feature-name \"Reactive Stash\"\n .feature-desc \"Deep state tree with path navigation. Every property tracked.\"\n\n .feature-card\n .feature-icon \"🎯\"\n .feature-name \"Fine-Grained Rendering\"\n .feature-desc \"No virtual DOM. Direct DOM updates via reactive effects.\"\n",
6
- "components/todos.rip": "# Todos Page\n\nexport Todos = component\n newTodo := ''\n todos := @app.data.todos or []\n nextId := (@app.data.nextId or 1)\n\n ~> @app.data.todos = todos\n ~> @app.data.nextId = nextId\n\n remaining ~= todos.filter((t) -> not t.done).length\n\n addTodo: ->\n if newTodo.trim()\n todos = [...todos, { id: nextId, text: newTodo.trim(), done: false }]\n nextId++\n newTodo = ''\n\n deleteTodo: (id) ->\n todos = todos.filter (t) -> t.id isnt id\n\n clearAll: ->\n todos = []\n\n handleKey: (e) ->\n if e.key is 'Enter'\n @addTodo()\n\n render\n div\n .page-header\n h1.page-title \"Todos\"\n p.page-subtitle \"List rendering and state management\"\n\n .card\n .todo-input-row\n input.input type: \"text\", value: newTodo, placeholder: \"What needs to be done?\", @keydown: @handleKey\n button.btn.btn-primary @click: @addTodo, \"Add\"\n\n ul.todo-list\n for todo in todos\n li.todo-item\n span.todo-text todo.text\n button.todo-delete @click: (=> @deleteTodo(todo.id)), \"x\"\n\n .todo-stats\n span \"#{remaining} remaining\"\n button.btn.btn-sm @click: @clearAll, \"Clear all\"\n",
5
+ "components/index.rip": "# Home Page\n\nexport Home = component\n\n render\n div\n .page-header\n h1.page-title \"Rip App Framework\"\n p.page-subtitle \"Zero-build reactive web apps\"\n\n .feature-grid\n .feature-card\n .feature-icon \"🗂\"\n .feature-name \"Component Store\"\n .feature-desc \"Component source files loaded on demand. Compiled in the browser.\"\n\n .feature-card\n .feature-icon \"🔀\"\n .feature-name \"File-Based Router\"\n .feature-desc \"URLs map to components. Dynamic segments and nested layouts.\"\n\n .feature-card\n .feature-icon \"⚡\"\n .feature-name \"Reactive Stash\"\n .feature-desc \"Deep state tree with path navigation. Every property tracked.\"\n\n .feature-card\n .feature-icon \"🎯\"\n .feature-name \"Fine-Grained Rendering\"\n .feature-desc \"No virtual DOM. Direct DOM updates via reactive effects.\"\n",
6
+ "components/todos.rip": "# Todos Page\n\nexport Todos = component\n newTodo := ''\n todos := @app.data.todos or []\n nextId := (@app.data.nextId or 1)\n\n ~> @app.data.todos = todos\n ~> @app.data.nextId = nextId\n\n remaining ~= todos.filter((t) -> not t.done).length\n\n addTodo: ->\n if newTodo.trim()\n todos = [...todos, { id: nextId, text: newTodo.trim(), done: false }]\n nextId++\n newTodo = ''\n\n deleteTodo: (id) ->\n todos = todos.filter (t) -> t.id isnt id\n\n clearAll: ->\n todos = []\n\n handleKey: (e) ->\n if e.key is 'Enter'\n @addTodo()\n\n render\n div\n .page-header\n h1.page-title \"Todos\"\n p.page-subtitle \"List rendering and state management\"\n\n .card\n .todo-input-row\n input.input type: \"text\", value <=> newTodo, placeholder: \"What needs to be done?\", @keydown: @handleKey\n button.btn.btn-primary @click: @addTodo, \"Add\"\n\n ul.todo-list\n for todo in todos\n li.todo-item\n span.todo-text todo.text\n button.todo-delete @click: (=> @deleteTodo(todo.id)), \"x\"\n\n .todo-stats\n span \"#{remaining} remaining\"\n button.btn.btn-sm @click: @clearAll, \"Clear all\"\n",
7
7
  "components/card.rip": "# Card — reusable content card component\n\nexport Card = component\n title =! \"\"\n\n render\n div.card\n if title\n h3 \"#{title}\"\n @children\n",
8
- "components/about.rip": "# About Page — uses the Card component for content sections\n\nexport About = component\n\n render\n div\n .page-header\n h1.page-title \"About Rip UI\"\n p.page-subtitle \"Zero-build reactive web apps!\"\n\n Card title: \"The Idea\"\n p \"Traditional frameworks build and bundle on the server, then ship static artifacts. Rip UI ships the 40KB compiler to the browser instead. Components arrive as .rip source files, are compiled on demand, and render with fine-grained reactivity. No build step, no bundler, no configuration files.\"\n\n Card title: \"Two Keywords\"\n p \"The component model adds exactly two keywords to the Rip language: component and render. Everything else — reactive state (:=), computed values (~=), effects (~>), methods, lifecycle hooks — is standard Rip syntax that already exists.\"\n\n Card title: \"Architecture\"\n p \"Components live as source files in the unified stash. The Router maps URLs to components using file-based conventions (like Next.js). When you navigate, the Renderer compiles the matching .rip file and mounts the resulting component. Each reactive binding creates a direct DOM effect — no virtual DOM diffing.\"\n\n Card title: \"Stack\"\n .btn-group\n .stat\n span \"Compiler \"\n span.stat-value \"40KB\"\n .stat\n span \"Framework \"\n span.stat-value \"~8KB\"\n .stat\n span \"Build step \"\n span.stat-value \"None\"\n .stat\n span \"Dependencies \"\n span.stat-value \"Zero\"\n\n Card title: \"Modules\"\n p \"The framework lives in one file: ui.rip. It uses Rip's built-in reactive primitives directly — the same signals that power := and ~= in components. Deep reactive stash with path navigation. Component store for source management. File-based router mapping URLs to components. Renderer that compiles and mounts components with layout support.\"\n",
9
- "components/_layout.rip": "# Root Layout (with error boundary and navigation indicator)\n\nexport Layout = component\n errorMsg := null\n\n ~> localStorage.setItem '__rip_route', @router.path\n\n onError: (err) ->\n errorMsg = err.message\n\n render\n .app-layout\n nav.app-nav\n .nav-inner\n a.nav-brand \"Rip UI\"\n .nav-links\n a.('nav-link', @router.path is '/' and 'active') href: \"#/\", \"Home\"\n a.('nav-link', @router.path is '/counter' and 'active') href: \"#counter\", \"Counter\"\n a.('nav-link', @router.path is '/todos' and 'active') href: \"#todos\", \"Todos\"\n a.('nav-link', @router.path is '/about' and 'active') href: \"#about\", \"About\"\n if @router.navigating\n span.nav-loading \"Loading...\"\n main.app-main\n if errorMsg\n .error-banner\n p \"Something went wrong: #{errorMsg}\"\n button.btn @click: (-> errorMsg = null), \"Dismiss\"\n #content\n"
8
+ "components/about.rip": "# About Page — uses the Card component for content sections\n\nexport About = component\n\n render\n div\n .page-header\n h1.page-title \"About Rip App\"\n p.page-subtitle \"Zero-build reactive web apps!\"\n\n Card title: \"The Idea\"\n p \"Traditional frameworks build and bundle on the server, then ship static artifacts. Rip App ships the 40KB compiler to the browser instead. Components arrive as .rip source files, are compiled on demand, and render with fine-grained reactivity. No build step, no bundler, no configuration files.\"\n\n Card title: \"Two Keywords\"\n p \"The component model adds exactly two keywords to the Rip language: component and render. Everything else — reactive state (:=), computed values (~=), effects (~>), methods, lifecycle hooks — is standard Rip syntax that already exists.\"\n\n Card title: \"Architecture\"\n p \"Components live as source files in the unified stash. The Router maps URLs to components using file-based conventions (like Next.js). When you navigate, the Renderer compiles the matching .rip file and mounts the resulting component. Each reactive binding creates a direct DOM effect — no virtual DOM diffing.\"\n\n Card title: \"Stack\"\n .btn-group\n .stat\n span \"Compiler \"\n span.stat-value \"40KB\"\n .stat\n span \"Framework \"\n span.stat-value \"~8KB\"\n .stat\n span \"Build step \"\n span.stat-value \"None\"\n .stat\n span \"Dependencies \"\n span.stat-value \"Zero\"\n\n Card title: \"Modules\"\n p \"The framework lives in one file: app.rip. It uses Rip's built-in reactive primitives directly — the same signals that power := and ~= in components. Deep reactive stash with path navigation. Component store for source management. File-based router mapping URLs to components. Renderer that compiles and mounts components with layout support.\"\n",
9
+ "components/_layout.rip": "# Root Layout (with error boundary and navigation indicator)\n\nexport Layout = component\n errorMsg := null\n\n ~> localStorage.setItem '__rip_route', @router.path\n\n onError: (err) ->\n errorMsg = err.message\n\n render\n .app-layout\n nav.app-nav\n .nav-inner\n a.nav-brand \"Rip App\"\n .nav-links\n a.('nav-link', @router.path is '/' and 'active') href: \"#/\", \"Home\"\n a.('nav-link', @router.path is '/counter' and 'active') href: \"#counter\", \"Counter\"\n a.('nav-link', @router.path is '/todos' and 'active') href: \"#todos\", \"Todos\"\n a.('nav-link', @router.path is '/about' and 'active') href: \"#about\", \"About\"\n if @router.navigating\n span.nav-loading \"Loading...\"\n main.app-main\n if errorMsg\n .error-banner\n p \"Something went wrong: #{errorMsg}\"\n button.btn @click: (-> errorMsg = null), \"Dismiss\"\n #content\n"
10
10
  },
11
11
  "data": {
12
- "title": "Rip UI Demo"
12
+ "title": "Rip App Demo"
13
13
  }
14
14
  }
@@ -36,15 +36,17 @@
36
36
  <p class="lead">A custom DuckDB extension repository. Install <code>ripdb</code> directly from this URL.</p>
37
37
 
38
38
  <h2>Install</h2>
39
- <pre><code>SET allow_unsigned_extensions = true;
39
+ <pre><code>$ duckdb -unsigned
40
40
  INSTALL ripdb FROM 'PAGE_BASE_URL';
41
41
  LOAD ripdb;</code></pre>
42
42
 
43
43
  <p>
44
- Custom-repository extensions are unsigned, so you need the
45
- <code>allow_unsigned_extensions</code> settingpass it on startup
46
- (<code>duckdb -unsigned</code>) or <code>SET</code> it at the top of your
47
- session before <code>LOAD</code>.
44
+ Custom-repository extensions are unsigned. Start DuckDB with the
45
+ <code>-unsigned</code> CLI flag that's the only supported way to
46
+ enable unsigned-extension support. <code>SET allow_unsigned_extensions
47
+ = true;</code> from a running session is rejected with
48
+ <em>"Cannot change allow_unsigned_extensions setting while database is
49
+ running."</em>
48
50
  </p>
49
51
 
50
52
  <h2>Currently published</h2>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extension": "ripdb",
3
3
  "base_url": "https://shreeve.github.io/rip-lang/extensions/duckdb",
4
- "updated_at": "2026-04-21T12:35:40Z",
4
+ "updated_at": "2026-04-27T04:14:31Z",
5
5
  "versions": {
6
6
  "v1.5.2": [
7
7
  "linux_amd64",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.14.4",
3
+ "version": "3.15.0",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
@@ -17,6 +17,9 @@
17
17
  },
18
18
  "./loader": "./rip-loader.js"
19
19
  },
20
+ "dependencies": {
21
+ "@rip-lang/schema": "0.1.0"
22
+ },
20
23
  "bin": {
21
24
  "rip": "bin/rip"
22
25
  },
@@ -34,6 +37,7 @@
34
37
  "bump": "bun scripts/bump.js",
35
38
  "gen:dom": "bun scripts/gen-dom.js",
36
39
  "gallery": "bun scripts/gallery.js",
40
+ "bundle:demo": "bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t 'Rip App Demo'",
37
41
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
38
42
  "postinstall": "bun scripts/link-local.js --quiet && bun scripts/link-check.js --quiet",
39
43
  "link-local": "bun scripts/link-local.js",
@@ -42,10 +46,14 @@
42
46
  "serve": "bun scripts/serve.js",
43
47
  "test": "bun test/runner.js",
44
48
  "test:types": "bun test/types/runner.js",
45
- "test:singleton": "bun test/schema-singleton.test.js",
49
+ "test:schema": "bun run --cwd packages/schema test",
50
+ "test:schema-fresh": "bun run --cwd packages/schema test:runtime-fresh",
51
+ "build:schema-runtime": "bun run --cwd packages/schema build:runtime",
52
+ "test:bundle": "bun test/bundle.test.js",
53
+ "test:graph": "bun scripts/check-bundle-graph.js",
46
54
  "test:server": "./bin/rip packages/server/tests/runner.rip",
47
55
  "test:time": "bun run --cwd packages/time test",
48
- "test:all": "bun run test && bun run test:types && bun run test:singleton && bun run test:server && bun run test:time",
56
+ "test:all": "bun run test && bun run test:types && bun run test:schema && bun run test:schema-fresh && bun run test:graph && bun run test:bundle && bun run test:server && bun run test:time",
49
57
  "test:ui": "bun run --cwd packages/ui test:e2e",
50
58
  "test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
51
59
  "test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
package/src/AGENTS.md CHANGED
@@ -1,6 +1,77 @@
1
1
  # Compiler Subsystem — Agent Guide
2
2
 
3
- This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `types.js`, `typecheck.js`, and the `grammar/` directory.
3
+ This covers `compiler.js`, `lexer.js`, `components.js`, `browser.js`, `types.js`, `types-emit.js`, `app.rip`, `typecheck.js`, and the `grammar/` directory. The schema feature lives in `packages/schema/` (entry point `packages/schema/src/schema.js`, imported here as `@rip-lang/schema`); see `packages/schema/README.md` for its layout.
4
+
5
+ ---
6
+
7
+ ## Module Map — browser-side vs CLI-only
8
+
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.
10
+
11
+ | Module | Browser? | Purpose |
12
+ | --- | --- | --- |
13
+ | `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 |
15
+ | `src/parser.js` | yes | generated LR table |
16
+ | `src/lexer.js` | yes | tokenizer + rewriter pipeline |
17
+ | `src/compiler.js` | yes | codegen + reactive runtime + component runtime + `compileToJS` + `setTypesEmitter` hook + `emitEnum` |
18
+ | `src/components.js` | yes | render rewriter + component runtime |
19
+ | `packages/schema/src/schema.js` | yes (via `@rip-lang/schema`) | lexer rewrite + body parser + emitSchema codegen + `setSchemaRuntimeProvider` hook (no fragment imports) |
20
+ | `packages/schema/src/loader-browser.js` | yes (browser only, via `@rip-lang/schema/loader-browser`) | imports validate + browser-stubs fragments; eager-installs browser runtime; registers provider |
21
+ | `packages/schema/src/loader-server.js` | **no** (CLI / server / tests, via `@rip-lang/schema/loader-server`) | imports all five fragments; eager-installs migration runtime; registers provider |
22
+ | `packages/schema/src/runtime.generated.js` | yes (browser uses 2 of 5 exports) | autogen from `runtime-*.js` fragments; CI staleness check via `bun run test:schema-fresh` |
23
+ | `packages/schema/src/runtime-validate.js` | source for the `validate` fragment (universal) |
24
+ | `packages/schema/src/runtime-db-naming.js` | source for `db-naming` fragment (server + migration) |
25
+ | `packages/schema/src/runtime-orm.js` | source for `orm` fragment (server + migration) |
26
+ | `packages/schema/src/runtime-ddl.js` | source for `ddl` fragment (migration only) |
27
+ | `packages/schema/src/runtime-browser-stubs.js` | source for `browser-stubs` fragment (browser only) |
28
+ | `src/types.js` | yes | only `installTypeSupport(Lexer)` — token-stream type stripper |
29
+ | `src/error.js` | yes | runtime error formatting |
30
+ | `src/sourcemaps.js` | yes | inline source-map generation |
31
+ | `src/generated/dom-tags.js` | yes | HTML/SVG tag set for render-block tag detection |
32
+ | `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
+ | `packages/schema/src/dts-emit.js` | **no** | schema `.d.ts` emitter — CLI / typecheck only |
35
+ | `src/typecheck.js` | **no** | TypeScript LSP integration — CLI only |
36
+ | `src/repl.js` | **no** | interactive CLI REPL |
37
+
38
+ The forbidden list in `scripts/check-bundle-graph.js` enforces this. If a code change would put a forbidden module on the browser graph, `bun run build` aborts before the bundler runs.
39
+
40
+ ### Registration-hook pattern (`setTypesEmitter`, `setSchemaRuntimeProvider`)
41
+
42
+ The same pattern is used twice — once for `.d.ts` emission, once for the schema runtime body. Both make the bundler's tree-shaker keep CLI/server-only code out of the browser bundle.
43
+
44
+ `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
+
46
+ `src/types-emit.js` calls `setTypesEmitter(emitTypes)` at module load. Any caller that wants `.d.ts` output side-effect-imports `types-emit.js`:
47
+
48
+ ```javascript
49
+ // CLI entry — bin/rip
50
+ import '../src/types-emit.js'; // installs emitter
51
+
52
+ // LSP integration
53
+ import { ... } from './types-emit.js';
54
+
55
+ // Test runner that exercises type emission
56
+ import '../src/types-emit.js';
57
+ ```
58
+
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.
60
+
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';`.
62
+
63
+ The schema runtime uses an analogous hook: `packages/schema/src/schema.js` exports `setSchemaRuntimeProvider(fn)`, default null. `packages/schema/src/loader-server.js` and `packages/schema/src/loader-browser.js` are the two providers. CLI / tests / server side-effect-import `@rip-lang/schema/loader-server` (full migration runtime, all four modes). The browser bundle (`src/browser.js`) side-effect-imports `@rip-lang/schema/loader-browser` (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
+
65
+ The mode matrix exposed by `getSchemaRuntime({ mode })`:
66
+
67
+ | mode | composition | typical caller |
68
+ | --- | --- | --- |
69
+ | `validate` | VALIDATE | isomorphic validate-only contexts |
70
+ | `browser` | VALIDATE + BROWSER_STUBS | the `<script type="text/rip">` runtime |
71
+ | `server` | VALIDATE + DB_NAMING + ORM | `@rip-lang/server` and friends |
72
+ | `migration` | VALIDATE + DB_NAMING + ORM + DDL | CLI / migration tool / tests (default) |
73
+
74
+ Edits to `packages/schema/src/runtime-*.js` require running `bun run build:schema-runtime` (delegates to the schema package) to regenerate `runtime.generated.js`. CI fails (`bun run test:schema-fresh`) if the generated file is stale.
4
75
 
5
76
  ---
6
77
 
@@ -552,12 +623,14 @@ enum Status
552
623
 
553
624
  ### Architecture
554
625
 
555
- Type emission logic lives in `types.js`. Type-checking integration and diagnostic filtering live in `typecheck.js`.
626
+ Type emission is split across two files by execution context:
627
+
628
+ - `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()`.
630
+
631
+ `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.
556
632
 
557
- - `installTypeSupport(Lexer)` adds `rewriteTypes()`
558
- - `emitTypes(tokens)` emits `.d.ts`
559
- - `emitEnum()` emits runtime JS for enums
560
- - `typecheck.js` drives `rip check` and mediates TypeScript diagnostics
633
+ `typecheck.js` (CLI only) drives `rip check`, mediates TypeScript diagnostics, and side-effect-imports `types-emit.js` for the intrinsic decl tables.
561
634
 
562
635
  Types are processed at the token layer before parsing.
563
636
 
@@ -569,8 +642,31 @@ Types are processed at the token layer before parsing.
569
642
 
570
643
  ## Schema System
571
644
 
572
- Inline schemas are a third compiler sidecar `schema.js` — that parallels
573
- `types.js` and `components.js`.
645
+ Inline schemas are a compiler sidecar that parallels `types.js` and
646
+ `components.js`, but unlike those two it lives in its own workspace
647
+ package — `packages/schema/`, imported here as `@rip-lang/schema`. The
648
+ implementation is split across several files by execution context:
649
+
650
+ - `packages/schema/src/schema.js` (browser + server) — lexer rewrite,
651
+ body parsers, `emitSchema` codegen, and `setSchemaRuntimeProvider`
652
+ hook. The runtime body itself is **not** here; this file imports zero
653
+ fragments so the bundler can decide per-entry which fragments to include.
654
+ - `packages/schema/src/runtime-{validate,db-naming,orm,ddl,browser-stubs}.js`
655
+ (sources) and `packages/schema/src/runtime.generated.js` (autogen) —
656
+ five runtime fragments composed at call time by `getSchemaRuntime({ mode })`.
657
+ Edit a source fragment, run `bun run build:schema-runtime` to refresh
658
+ the generated file, commit. CI's `test:schema-fresh` fails on staleness.
659
+ - `@rip-lang/schema/loader-server` and `@rip-lang/schema/loader-browser`
660
+ — the import boundary that decides which fragments end up in which
661
+ bundle. Server loader pulls all five; browser loader pulls only
662
+ validate + browser-stubs. Bun's tree-shaker uses these import sets to
663
+ omit server-only fragments from `docs/dist/rip.min.js`.
664
+ - `@rip-lang/schema/dts-emit` (CLI/LSP only) — `emitSchemaTypes` walks
665
+ parsed schema s-expressions and emits `declare const Foo: Schema<...>`
666
+ lines for the TypeScript language service. Imported only by
667
+ `types-emit.js` and `typecheck.js`. Lives inside `packages/schema/src/`
668
+ so all schema-related code is colocated; the `dts-emit` name signals
669
+ that this is a compile-time `.d.ts` emitter, not a `runtime-*` fragment.
574
670
 
575
671
  ### Lexer path
576
672
 
@@ -669,7 +765,7 @@ signatures both enforce this.
669
765
  `@ensure` entirely (trusted data). Runtime method name
670
766
  `_applyEnsures` mirrors the directive (parallel to
671
767
  `_applyTransforms` for `-> transform` and `_applyEagerDerived` for
672
- `!> derived`). See `src/schema.js`.
768
+ `!> derived`). See `packages/schema/src/schema.js`.
673
769
 
674
770
  ### Shadow TS
675
771
 
@@ -761,7 +761,16 @@ compileAndImport = (source, compile, components = null, path = null, resolver =
761
761
  resolver.compiling ?= {}
762
762
  resolver.compiling[path] = true
763
763
 
764
- js = compile(source)
764
+ # Browser-debugger support — when enabled, compile with an inline source
765
+ # map keyed to the component's logical path. We still need to compensate
766
+ # for line shifts introduced by the header + preamble we prepend below;
767
+ # `prefixLines` tracks that and we apply `offsetSourceMap` once at the end.
768
+ debug = globalThis?.__ripDebug?.enabled and path
769
+ prefixLines = 0
770
+ js = if debug
771
+ compile(source, sourceMap: 'inline', filename: path)
772
+ else
773
+ compile(source)
765
774
 
766
775
  if resolver
767
776
  importedNames = new Set()
@@ -812,9 +821,21 @@ compileAndImport = (source, compile, components = null, path = null, resolver =
812
821
  if names.length > 0
813
822
  preamble = "const {#{names.join(', ')}} = globalThis['#{resolver.key}'];\n"
814
823
  js = preamble + js
824
+ prefixLines += 1
815
825
 
816
826
  header = if path then "// #{path}\n" else ''
817
- blob = new Blob([header + js], { type: 'application/javascript' })
827
+ prefixLines += 1 if header
828
+ finalJs = header + js
829
+
830
+ # Source-map post-processing: prepend N semicolons to map.mappings so
831
+ # generated lines line up with `header + preamble + js`. The Blob URL
832
+ # itself becomes the source identity in DevTools, so an explicit
833
+ # //# sourceURL pragma isn't needed here (only for eval'd code).
834
+ if debug and prefixLines > 0
835
+ offset = globalThis?.__ripDebug?.offsetSourceMap
836
+ finalJs = offset(finalJs, prefixLines) if offset
837
+
838
+ blob = new Blob([finalJs], { type: 'application/javascript' })
818
839
  url = URL.createObjectURL(blob)
819
840
 
820
841
  # Cache blob URL so other files can rewrite imports to point here
@@ -1171,6 +1192,7 @@ export launch = (appBase = '', opts = {}) ->
1171
1192
  components: appComponents
1172
1193
  router: router
1173
1194
  renderer: renderer
1195
+ resolver: resolver
1174
1196
  cache: renderer.cache
1175
1197
  version: '0.3.0'
1176
1198
 
package/src/browser.js CHANGED
@@ -1,6 +1,12 @@
1
1
  // Browser-compatible entry point for Rip compiler
2
2
  // Includes runtime for <script type="text/rip"> support
3
3
 
4
+ // Side-effect import — registers the BROWSER schema runtime provider.
5
+ // Pulls in only the validate + browser-stubs fragments; tree-shakes
6
+ // db-naming, orm, and ddl fragments out of the bundle. Must be the
7
+ // first import so any later module-load eager-installs see it.
8
+ import '@rip-lang/schema/loader-browser';
9
+
4
10
  export { Lexer } from './lexer.js';
5
11
  export { parser } from './parser.js';
6
12
  export { CodeEmitter, Compiler, compile, compileToJS, formatSExpr, getStdlibCode, getReactiveRuntime, getComponentRuntime, RipError, formatError, formatErrorHTML } from './compiler.js';
@@ -14,7 +20,7 @@ export const BUILD_DATE = "0000-00-00@00:00:00GMT";
14
20
  import { compile, compileToJS, formatSExpr, getReactiveRuntime, getComponentRuntime } from './compiler.js';
15
21
 
16
22
  // Eagerly register Rip's reactive and component runtimes on globalThis so that
17
- // framework code (ui.rip) and browser-compiled scripts can use them directly
23
+ // framework code (app.rip) and browser-compiled scripts can use them directly
18
24
  if (typeof globalThis !== 'undefined') {
19
25
  if (!globalThis.__rip) new Function(getReactiveRuntime())();
20
26
  if (!globalThis.__ripComponent) new Function(getComponentRuntime())();
@@ -26,6 +32,90 @@ const dedent = s => {
26
32
  return s.replace(RegExp(`^[ \t]{${i}}`, 'gm'), '').trim();
27
33
  }
28
34
 
35
+ // ---------------------------------------------------------------------------
36
+ // Source-map helpers — for browser-side debugger + DevTools navigation.
37
+ //
38
+ // `compileToJS(src, { sourceMap: 'inline', filename: <name>.rip })` returns
39
+ // JS with a trailing `//# sourceMappingURL=data:application/json;base64,...`
40
+ // comment. We add a leading `//# sourceURL=<name>.rip.js` so DevTools shows
41
+ // the eval'd code as a navigable virtual file (named differently than the
42
+ // source-map's `sources[]` entry to avoid DevTools merging entries).
43
+ //
44
+ // CR/LF in the `sourceURL` would let an attacker inject another pragma; sanitize.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const sanitizeSourceURL = (url) =>
48
+ String(url).replace(/[\r\n]/g, '').replace(/\s+$/g, '');
49
+
50
+ // Insert `//# sourceURL=<name>` BEFORE the existing `//# sourceMappingURL=...`
51
+ // comment (or append at end if none). NEVER prepend — that would shift every
52
+ // generated-line mapping by 1 line, breaking line-only source maps.
53
+ function addSourceURL(js, generatedName) {
54
+ const safe = sanitizeSourceURL(generatedName);
55
+ const pragma = `//# sourceURL=${safe}`;
56
+ const mapRe = /\n?\/\/# sourceMappingURL=[^\n]*\s*$/;
57
+ const m = js.match(mapRe);
58
+ if (m) return js.slice(0, m.index) + '\n' + pragma + js.slice(m.index);
59
+ return js + '\n' + pragma;
60
+ }
61
+
62
+ // Shift all generated-line mappings by N lines by prepending N semicolons to
63
+ // the `mappings` field. Each `;` in source-map V3 mappings represents an empty
64
+ // generated line. Used to compensate for a runtime async IIFE wrapper that
65
+ // adds N lines BEFORE the compiled code.
66
+ function offsetSourceMap(js, offsetLines) {
67
+ if (!offsetLines) return js;
68
+ return js.replace(
69
+ /\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/,
70
+ (_, b64) => {
71
+ let json;
72
+ try {
73
+ // UTF-8-safe decode: counterpart of the encode in compiler.js. The
74
+ // map JSON may contain non-ASCII chars (sourcesContent), so we go
75
+ // bytes -> string via TextDecoder.
76
+ const bin = atob(b64);
77
+ const bytes = new Uint8Array(bin.length);
78
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
79
+ json = new TextDecoder().decode(bytes);
80
+ } catch { return _; /* leave unchanged on decode failure */ }
81
+ let map;
82
+ try { map = JSON.parse(json); } catch { return _; }
83
+ map.mappings = ';'.repeat(offsetLines) + map.mappings;
84
+ // UTF-8-safe re-encode.
85
+ const bytes = new TextEncoder().encode(JSON.stringify(map));
86
+ let out = '';
87
+ for (let i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]);
88
+ return `//# sourceMappingURL=data:application/json;base64,${btoa(out)}`;
89
+ }
90
+ );
91
+ }
92
+
93
+ // Wrap compiled JS in an async IIFE for top-level await support, applying the
94
+ // 1-line source-map offset that the wrapper introduces, AND adding a
95
+ // `sourceURL` pragma so DevTools shows the eval'd code with a sensible name.
96
+ function wrapForEval(js, ripName) {
97
+ const generatedName = `${sanitizeSourceURL(ripName)}.js`;
98
+ const shifted = offsetSourceMap(js, 1);
99
+ const tagged = addSourceURL(shifted, generatedName);
100
+ return `(async()=>{\n${tagged}\n})()`;
101
+ }
102
+
103
+ // Expose the helpers on globalThis so `app.rip` (compiled separately into
104
+ // the bundle) can apply the same source-map post-processing to its
105
+ // component-load path. The `enabled` flag is owned by processRipScripts —
106
+ // when it reads `data-debug` from the runtime <script> tag, it sets this
107
+ // flag accordingly. Code paths outside processRipScripts (notably
108
+ // `app.launch()`'s component compile path) gate their source-map work on
109
+ // __ripDebug.enabled.
110
+ if (typeof globalThis !== 'undefined') {
111
+ globalThis.__ripDebug = {
112
+ enabled: true, // default ON — processRipScripts may flip to false
113
+ offsetSourceMap,
114
+ addSourceURL,
115
+ sanitizeSourceURL,
116
+ };
117
+ }
118
+
29
119
  // Browser runtime: collect all sources (inline scripts, data-src files, bundles),
30
120
  // compile them in a shared scope, and execute as one async IIFE.
31
121
  //
@@ -85,27 +175,35 @@ async function processRipScripts() {
85
175
  // for full routing support. Otherwise compile everything upfront.
86
176
  if (hasRouter && bundles.length > 0) {
87
177
  // Compile non-bundle sources (inline scripts, individual .rip files)
88
- const opts = { skipRuntimes: true, skipExports: true, skipImports: true };
89
- if (individual.length > 0) {
90
- let js = '';
91
- for (const s of individual) {
92
- try { js += compileToJS(s.code, opts) + '\n'; }
93
- catch (e) { console.error(_formatError(e, { source: s.code, file: s.url || 'inline', color: false })); }
94
- }
95
- if (js) {
96
- try { await (0, eval)(`(async()=>{\n${js}\n})()`); }
97
- catch (e) { console.error('Rip runtime error:', e); }
98
- }
178
+ // with per-component source maps for browser-debugger support. The
179
+ // bundle itself is launched separately via `app.launch(bundle)`
180
+ // its components get source maps too via `globalThis.__ripDebug`,
181
+ // which `app.rip`'s component-compile path reads to apply the same
182
+ // offset+sourceURL treatment we apply here.
183
+ const debug = runtimeTag?.getAttribute('data-debug') !== 'false';
184
+ if (globalThis.__ripDebug) globalThis.__ripDebug.enabled = debug;
185
+ const baseOpts = { skipRuntimes: true, skipExports: true, skipImports: true };
186
+ let inlineCounter = 0;
187
+ for (const s of individual) {
188
+ const ripName = s.url || `inline-${++inlineCounter}.rip`;
189
+ const opts = debug
190
+ ? { ...baseOpts, sourceMap: 'inline', filename: ripName }
191
+ : baseOpts;
192
+ let js;
193
+ try { js = compileToJS(s.code, opts); }
194
+ catch (e) { console.error(_formatError(e, { source: s.code, file: ripName, color: false })); continue; }
195
+ try { await (0, eval)(debug ? wrapForEval(js, ripName) : `(async()=>{\n${js}\n})()`); }
196
+ catch (e) { console.error(`Rip runtime error in ${ripName}:`, e); }
99
197
  }
100
198
 
101
199
  // Launch with the last bundle (app bundle) — handles router, renderer, stash
102
- const ui = importRip.modules?.['ui.rip'];
103
- if (ui?.launch) {
200
+ const app = importRip.modules?.['app.rip'];
201
+ if (app?.launch) {
104
202
  const appBundle = bundles[bundles.length - 1];
105
203
  const persistAttr = runtimeTag.getAttribute('data-persist');
106
204
  const launchOpts = { bundle: appBundle, hash: routerAttr === 'hash' };
107
205
  if (persistAttr != null) launchOpts.persist = persistAttr === 'local' ? 'local' : true;
108
- await ui.launch('', launchOpts);
206
+ await app.launch('', launchOpts);
109
207
  }
110
208
  } else {
111
209
  // No routing — expand bundles into individual sources, compile everything
@@ -124,15 +222,29 @@ async function processRipScripts() {
124
222
  }
125
223
  expanded.push(...individual);
126
224
 
127
- const opts = { skipRuntimes: true, skipExports: true, skipImports: true };
225
+ // Source maps are ON by default `data-debug="false"` opts out.
226
+ // Individually compile each source with its own source map; we'll
227
+ // sequentially eval them per-component so each has a self-consistent
228
+ // map (DevTools only honours the last sourceMappingURL inside an
229
+ // eval, so concatenating maps doesn't work).
230
+ const debug = runtimeTag?.getAttribute('data-debug') !== 'false';
231
+ // Update the global flag so app.launch()'s compile path (in app.rip)
232
+ // sees the same setting as our local `debug` variable.
233
+ if (globalThis.__ripDebug) globalThis.__ripDebug.enabled = debug;
234
+ const baseOpts = { skipRuntimes: true, skipExports: true, skipImports: true };
128
235
  const compiled = [];
236
+ let inlineCounter = 0;
129
237
  for (const s of expanded) {
130
238
  if (!s.code) continue;
239
+ const ripName = s.url || `inline-${++inlineCounter}.rip`;
240
+ const opts = debug
241
+ ? { ...baseOpts, sourceMap: 'inline', filename: ripName }
242
+ : baseOpts;
131
243
  try {
132
244
  const js = compileToJS(s.code, opts);
133
- compiled.push({ js, url: s.url || 'inline' });
245
+ compiled.push({ js, url: ripName });
134
246
  } catch (e) {
135
- console.error(_formatError(e, { source: s.code, file: s.url || 'inline', color: false }));
247
+ console.error(_formatError(e, { source: s.code, file: ripName, color: false }));
136
248
  }
137
249
  }
138
250
 
@@ -157,30 +269,35 @@ async function processRipScripts() {
157
269
  }
158
270
  }
159
271
 
160
- // Execute all compiled code in shared scope
272
+ // Execute compiled code per-component so each has its own valid
273
+ // source map (DevTools only honours the last `sourceMappingURL`
274
+ // pragma inside one evaluated chunk, so concatenating multiple
275
+ // source-mapped chunks would lose all but the last). Components
276
+ // share scope via globalThis attachment — typical Rip definitions
277
+ // (`Foo = component`, `Bar = ...`) compile to globalThis-attached
278
+ // bindings under `skipExports/skipImports`, which survive between
279
+ // the per-component evals.
161
280
  if (compiled.length > 0) {
162
- let js = compiled.map(c => c.js).join('\n');
281
+ let anyError = false;
282
+ for (const c of compiled) {
283
+ try {
284
+ await (0, eval)(debug ? wrapForEval(c.js, c.url) : `(async()=>{\n${c.js}\n})()`);
285
+ } catch (e) {
286
+ anyError = true;
287
+ if (e instanceof SyntaxError) console.error(`Rip syntax error in ${c.url}: ${e.message}`);
288
+ else console.error(`Rip runtime error in ${c.url}:`, e);
289
+ }
290
+ }
163
291
 
292
+ // Final mount step — runs after all components are defined.
164
293
  const mount = runtimeTag?.getAttribute('data-mount');
165
294
  if (mount) {
166
295
  const target = runtimeTag.getAttribute('data-target') || 'body';
167
- js += `\n${mount}.mount(${JSON.stringify(target)});`;
296
+ try { await (0, eval)(`(async()=>{ ${mount}.mount(${JSON.stringify(target)}); })()`); }
297
+ catch (e) { console.error(`Rip mount error (${mount}):`, e); }
168
298
  }
169
299
 
170
- try {
171
- await (0, eval)(`(async()=>{\n${js}\n})()`);
172
- document.body.classList.add('ready');
173
- } catch (e) {
174
- if (e instanceof SyntaxError) {
175
- console.error(`Rip syntax error in combined output: ${e.message}`);
176
- for (const c of compiled) {
177
- try { new Function(`(async()=>{\n${c.js}\n})()`); }
178
- catch (e2) { console.error(` → source: ${c.url}`, e2.message); }
179
- }
180
- } else {
181
- console.error('Rip runtime error:', e);
182
- }
183
- }
300
+ if (!anyError) document.body.classList.add('ready');
184
301
  }
185
302
  }
186
303
  }
@@ -232,10 +349,10 @@ export { processRipScripts };
232
349
  /**
233
350
  * Import a .rip file as an ES module
234
351
  * Fetches the URL, compiles Rip→JS, dynamically imports via Blob URL
235
- * Usage: const { launch } = await importRip('/ui.rip')
352
+ * Usage: const { launch } = await importRip('/app.rip')
236
353
  *
237
354
  * Pre-compiled modules can be registered on importRip.modules to skip fetching.
238
- * The browser bundle uses this to embed ui.rip without a server round-trip.
355
+ * The browser bundle uses this to embed app.rip without a server round-trip.
239
356
  */
240
357
  export async function importRip(url) {
241
358
  for (const [key, mod] of Object.entries(importRip.modules)) {