roqa 0.0.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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +77 -0
- package/src/compiler/codegen.js +1217 -0
- package/src/compiler/index.js +47 -0
- package/src/compiler/parser.js +197 -0
- package/src/compiler/transforms/bind-detector.js +264 -0
- package/src/compiler/transforms/events.js +246 -0
- package/src/compiler/transforms/for-transform.js +164 -0
- package/src/compiler/transforms/inline-get.js +1049 -0
- package/src/compiler/transforms/jsx-to-template.js +871 -0
- package/src/compiler/transforms/show-transform.js +78 -0
- package/src/compiler/transforms/validate.js +80 -0
- package/src/compiler/utils.js +69 -0
- package/src/jsx-runtime.d.ts +640 -0
- package/src/jsx-runtime.js +73 -0
- package/src/runtime/cell.js +37 -0
- package/src/runtime/component.js +241 -0
- package/src/runtime/events.js +156 -0
- package/src/runtime/for-block.js +374 -0
- package/src/runtime/index.js +17 -0
- package/src/runtime/show-block.js +115 -0
- package/src/runtime/template.js +32 -0
- package/types/compiler.d.ts +9 -0
- package/types/index.d.ts +433 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Create a cell (reactive value container)
|
|
2
|
+
// Compiler will inline as { v: v, e: [] }
|
|
3
|
+
export const cell = (v) => ({ v, e: [] });
|
|
4
|
+
|
|
5
|
+
// Get cell value
|
|
6
|
+
// Compiler can inline as s.v
|
|
7
|
+
export const get = (s) => s.v;
|
|
8
|
+
|
|
9
|
+
// Set cell value with notification (reactive update)
|
|
10
|
+
// Compiler will inline as s.v = v and notification code and/or loop
|
|
11
|
+
export const set = (cell, v) => {
|
|
12
|
+
cell.v = v;
|
|
13
|
+
for (let i = 0; i < cell.e.length; i++) cell.e[i](v);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Set cell value without notification
|
|
17
|
+
// Compiler will inline as s.v = v
|
|
18
|
+
export const put = (s, v) => {
|
|
19
|
+
s.v = v;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Bind an effect to a cell (for reactive DOM updates)
|
|
23
|
+
// Calls the effect immediately with the current value, then on each change
|
|
24
|
+
// Returns an unsubscribe function for cleanup
|
|
25
|
+
export const bind = (cell, fn) => {
|
|
26
|
+
fn(cell.v); // Run immediately with current value
|
|
27
|
+
cell.e.push(fn);
|
|
28
|
+
return () => {
|
|
29
|
+
const idx = cell.e.indexOf(fn);
|
|
30
|
+
if (idx > -1) cell.e.splice(idx, 1);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Notify all effects bound to a cell
|
|
35
|
+
export const notify = (cell) => {
|
|
36
|
+
for (let i = 0; i < cell.e.length; i++) cell.e[i](cell.v);
|
|
37
|
+
};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { handleRootEvents } from "./events.js";
|
|
2
|
+
|
|
3
|
+
// Register event delegation once at module load
|
|
4
|
+
handleRootEvents(document);
|
|
5
|
+
|
|
6
|
+
// WeakMap to store props for elements before they're upgraded
|
|
7
|
+
const elementProps = new WeakMap();
|
|
8
|
+
|
|
9
|
+
// Set of known element property names to exclude when collecting props
|
|
10
|
+
const EXCLUDED_PROPS = new Set([
|
|
11
|
+
// Standard HTMLElement properties
|
|
12
|
+
"accessKey",
|
|
13
|
+
"autocapitalize",
|
|
14
|
+
"autofocus",
|
|
15
|
+
"className",
|
|
16
|
+
"contentEditable",
|
|
17
|
+
"dir",
|
|
18
|
+
"draggable",
|
|
19
|
+
"enterKeyHint",
|
|
20
|
+
"hidden",
|
|
21
|
+
"id",
|
|
22
|
+
"inert",
|
|
23
|
+
"innerText",
|
|
24
|
+
"inputMode",
|
|
25
|
+
"lang",
|
|
26
|
+
"nonce",
|
|
27
|
+
"outerText",
|
|
28
|
+
"popover",
|
|
29
|
+
"spellcheck",
|
|
30
|
+
"style",
|
|
31
|
+
"tabIndex",
|
|
32
|
+
"title",
|
|
33
|
+
"translate",
|
|
34
|
+
// Common DOM properties
|
|
35
|
+
"innerHTML",
|
|
36
|
+
"outerHTML",
|
|
37
|
+
"textContent",
|
|
38
|
+
"nodeValue",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set a prop on an element (works before or after upgrade)
|
|
43
|
+
*/
|
|
44
|
+
export function setProp(element, propName, value) {
|
|
45
|
+
let props = elementProps.get(element);
|
|
46
|
+
if (!props) {
|
|
47
|
+
props = {};
|
|
48
|
+
elementProps.set(element, props);
|
|
49
|
+
}
|
|
50
|
+
props[propName] = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all props for an element
|
|
55
|
+
* Merges props from WeakMap with any properties set directly on the element
|
|
56
|
+
*/
|
|
57
|
+
export function getProps(element) {
|
|
58
|
+
const weakMapProps = elementProps.get(element);
|
|
59
|
+
const ownKeys = Object.keys(element);
|
|
60
|
+
// Fast path: No props at all
|
|
61
|
+
if (!weakMapProps && ownKeys.length === 0) return {};
|
|
62
|
+
// Collect own properties set directly on the element (not inherited)
|
|
63
|
+
const directProps = {};
|
|
64
|
+
for (let i = 0; i < ownKeys.length; i++) {
|
|
65
|
+
const key = ownKeys[i];
|
|
66
|
+
if (!EXCLUDED_PROPS.has(key) && key[0] !== "_") {
|
|
67
|
+
directProps[key] = element[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Direct props take precedence (they're set closer to usage time)
|
|
71
|
+
return weakMapProps ? { ...weakMapProps, ...directProps } : directProps;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Base class for Roqa components with shared methods
|
|
76
|
+
*/
|
|
77
|
+
class RoqaElement extends HTMLElement {
|
|
78
|
+
_connectedCallbacks = [];
|
|
79
|
+
_disconnectedCallbacks = [];
|
|
80
|
+
_abortController = null;
|
|
81
|
+
_attrCallbacks = null;
|
|
82
|
+
connected(fn) {
|
|
83
|
+
this._connectedCallbacks.push(fn);
|
|
84
|
+
}
|
|
85
|
+
disconnected(fn) {
|
|
86
|
+
this._disconnectedCallbacks.push(fn);
|
|
87
|
+
}
|
|
88
|
+
on(eventName, handler) {
|
|
89
|
+
// Lazy initialization of AbortController
|
|
90
|
+
if (!this._abortController) {
|
|
91
|
+
this._abortController = new AbortController();
|
|
92
|
+
}
|
|
93
|
+
this.addEventListener(eventName, handler, { signal: this._abortController.signal });
|
|
94
|
+
}
|
|
95
|
+
emit(eventName, detail = undefined, options = {}) {
|
|
96
|
+
const { bubbles = true, composed = false } = options;
|
|
97
|
+
this.dispatchEvent(new CustomEvent(eventName, { detail, bubbles, composed }));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Toggle a boolean attribute based on a condition.
|
|
101
|
+
* When true, the attribute is present (empty string value).
|
|
102
|
+
* When false, the attribute is removed.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} name - The attribute name
|
|
105
|
+
* @param {boolean} condition - Whether the attribute should be present
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* this.toggleAttr('disabled', isDisabled);
|
|
109
|
+
* // true -> <my-element disabled>
|
|
110
|
+
* // false -> <my-element>
|
|
111
|
+
*/
|
|
112
|
+
toggleAttr(name, condition) {
|
|
113
|
+
if (condition) {
|
|
114
|
+
this.setAttribute(name, "");
|
|
115
|
+
} else {
|
|
116
|
+
this.removeAttribute(name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Set a state attribute with mutually exclusive on/off variants.
|
|
121
|
+
* Creates a pair of attributes where only one is present at a time.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} name - The base attribute name
|
|
124
|
+
* @param {boolean} condition - The state value
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* this.stateAttr('checked', isChecked);
|
|
128
|
+
* // true -> <my-element checked>
|
|
129
|
+
* // false -> <my-element unchecked>
|
|
130
|
+
*/
|
|
131
|
+
stateAttr(name, condition) {
|
|
132
|
+
const offName = "un" + name;
|
|
133
|
+
if (condition) {
|
|
134
|
+
this.setAttribute(name, "");
|
|
135
|
+
this.removeAttribute(offName);
|
|
136
|
+
} else {
|
|
137
|
+
this.removeAttribute(name);
|
|
138
|
+
this.setAttribute(offName, "");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Register a callback for when an observed attribute changes.
|
|
143
|
+
* The attribute must be declared in the `observedAttributes` option of defineComponent.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} name - The attribute name (must be in observedAttributes)
|
|
146
|
+
* @param {(newValue: string | null, oldValue: string | null) => void} callback - Called when attribute changes
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // In defineComponent:
|
|
150
|
+
* defineComponent("my-switch", MySwitch, { observedAttributes: ["checked"] });
|
|
151
|
+
*
|
|
152
|
+
* // In component:
|
|
153
|
+
* this.attrChanged("checked", (newValue, oldValue) => {
|
|
154
|
+
* console.log("checked changed from", oldValue, "to", newValue);
|
|
155
|
+
* });
|
|
156
|
+
*/
|
|
157
|
+
attrChanged(name, callback) {
|
|
158
|
+
if (!this._attrCallbacks) {
|
|
159
|
+
this._attrCallbacks = new Map();
|
|
160
|
+
}
|
|
161
|
+
let callbacks = this._attrCallbacks.get(name);
|
|
162
|
+
if (!callbacks) {
|
|
163
|
+
callbacks = [];
|
|
164
|
+
this._attrCallbacks.set(name, callbacks);
|
|
165
|
+
}
|
|
166
|
+
callbacks.push(callback);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Define a custom element component.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} tagName - The custom element tag name (must contain a hyphen)
|
|
174
|
+
* @param {Function} fn - The component function
|
|
175
|
+
* @param {Object} [options] - Optional configuration
|
|
176
|
+
* @param {string[]} [options.observedAttributes] - Attributes to observe for changes
|
|
177
|
+
* @param {boolean} [options.formAssociated] - Whether the element participates in form submission
|
|
178
|
+
*/
|
|
179
|
+
export function defineComponent(tagName, fn, options = {}) {
|
|
180
|
+
if (customElements.get(tagName)) return;
|
|
181
|
+
const observedAttrs = options.observedAttributes || [];
|
|
182
|
+
const formAssociated = options.formAssociated || false;
|
|
183
|
+
|
|
184
|
+
customElements.define(
|
|
185
|
+
tagName,
|
|
186
|
+
class extends RoqaElement {
|
|
187
|
+
static get observedAttributes() {
|
|
188
|
+
return observedAttrs;
|
|
189
|
+
}
|
|
190
|
+
static get formAssociated() {
|
|
191
|
+
return formAssociated;
|
|
192
|
+
}
|
|
193
|
+
constructor() {
|
|
194
|
+
super();
|
|
195
|
+
// Initialize ElementInternals for form-associated elements
|
|
196
|
+
if (formAssociated) {
|
|
197
|
+
this.internals = this.attachInternals();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
connectedCallback() {
|
|
201
|
+
const props = getProps(this);
|
|
202
|
+
fn.call(this, props);
|
|
203
|
+
const cbs = this._connectedCallbacks;
|
|
204
|
+
for (let i = 0; i < cbs.length; i++) {
|
|
205
|
+
const cleanup = cbs[i]();
|
|
206
|
+
if (typeof cleanup === "function") {
|
|
207
|
+
this._disconnectedCallbacks.push(cleanup);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
disconnectedCallback() {
|
|
212
|
+
const cbs = this._disconnectedCallbacks;
|
|
213
|
+
for (let i = 0; i < cbs.length; i++) cbs[i]();
|
|
214
|
+
if (this._abortController) this._abortController.abort();
|
|
215
|
+
}
|
|
216
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
217
|
+
// Only fire callbacks if value actually changed
|
|
218
|
+
if (oldValue === newValue) return;
|
|
219
|
+
const callbacks = this._attrCallbacks?.get(name);
|
|
220
|
+
if (callbacks) {
|
|
221
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
222
|
+
callbacks[i](newValue, oldValue);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Form-associated lifecycle callbacks
|
|
227
|
+
formResetCallback() {
|
|
228
|
+
// Dispatch event so components can handle form reset
|
|
229
|
+
this.dispatchEvent(new Event("form-reset"));
|
|
230
|
+
}
|
|
231
|
+
formDisabledCallback(disabled) {
|
|
232
|
+
// Dispatch event so components can handle form disabled state
|
|
233
|
+
this.dispatchEvent(new CustomEvent("form-disabled", { detail: { disabled } }));
|
|
234
|
+
}
|
|
235
|
+
formStateRestoreCallback(state, mode) {
|
|
236
|
+
// Dispatch event so components can restore form state
|
|
237
|
+
this.dispatchEvent(new CustomEvent("form-state-restore", { detail: { state, mode } }));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Event delegation primitives
|
|
2
|
+
//
|
|
3
|
+
// Instead of attaching listeners to every element, we attach one listener
|
|
4
|
+
// per event type to the document (or shadow root). When an event fires, we
|
|
5
|
+
// walk up the DOM path looking for elements with __eventname properties.
|
|
6
|
+
//
|
|
7
|
+
// Handlers are stored as:
|
|
8
|
+
// - __click = fn -> fn.call(element, event)
|
|
9
|
+
// - __click = [fn, arg1, arg2] -> fn.call(element, arg1, arg2, event)
|
|
10
|
+
//
|
|
11
|
+
// The array form avoids closure allocation in loops (e.g., <For> items).
|
|
12
|
+
//
|
|
13
|
+
// Heavily based on Ripple's event delegation system: https://github.com/Ripple-TS/ripple/blob/main/packages/ripple/src/runtime/internal/client/events.js
|
|
14
|
+
|
|
15
|
+
// Touch events should be passive for scroll performance
|
|
16
|
+
const PASSIVE_EVENTS = new Set(["touchstart", "touchmove"]);
|
|
17
|
+
|
|
18
|
+
// Global registry of event types that need delegation (e.g., "click", "input")
|
|
19
|
+
const allRegisteredEvents = new Set();
|
|
20
|
+
|
|
21
|
+
// Callbacks to notify when new event types are registered
|
|
22
|
+
const rootEventHandles = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Core delegation handler - attached to document/shadow roots.
|
|
26
|
+
* Walks the composed path manually to find and invoke __eventname handlers.
|
|
27
|
+
*/
|
|
28
|
+
function handleEventPropagation(event) {
|
|
29
|
+
const handlerElement = this; // The root we're attached to (document or shadow root)
|
|
30
|
+
const path = event.composedPath();
|
|
31
|
+
let currentTarget = path[0] || event.target;
|
|
32
|
+
let pathIdx = 0;
|
|
33
|
+
const handledAt = event.__root;
|
|
34
|
+
|
|
35
|
+
// Prevent double-handling when event crosses shadow boundaries.
|
|
36
|
+
// __root marks where we've already processed this event.
|
|
37
|
+
if (handledAt) {
|
|
38
|
+
const atIdx = path.indexOf(handledAt);
|
|
39
|
+
if (atIdx !== -1 && handlerElement === document) {
|
|
40
|
+
event.__root = handlerElement;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const handlerIdx = path.indexOf(handlerElement);
|
|
44
|
+
if (handlerIdx === -1) return;
|
|
45
|
+
if (atIdx <= handlerIdx) pathIdx = atIdx;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if ((currentTarget = path[pathIdx] || event.target) === handlerElement) return;
|
|
49
|
+
|
|
50
|
+
// Override currentTarget to reflect the element we're currently processing
|
|
51
|
+
Object.defineProperty(event, "currentTarget", {
|
|
52
|
+
configurable: true,
|
|
53
|
+
get: () => currentTarget || handlerElement.ownerDocument,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const eventKey = "__" + event.type;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Walk up the DOM tree, checking each element for a delegated handler
|
|
60
|
+
for (; currentTarget; ) {
|
|
61
|
+
const parentElement =
|
|
62
|
+
currentTarget.assignedSlot || currentTarget.parentNode || currentTarget.host || null;
|
|
63
|
+
const delegated = currentTarget[eventKey];
|
|
64
|
+
try {
|
|
65
|
+
if (delegated && !currentTarget.disabled) {
|
|
66
|
+
if (Array.isArray(delegated)) {
|
|
67
|
+
// Array form: [fn, ...args] - used in loops to avoid closures
|
|
68
|
+
const fn = delegated[0];
|
|
69
|
+
const len = delegated.length;
|
|
70
|
+
// Optimized paths for common arities (1-3 args + event)
|
|
71
|
+
if (len === 2) {
|
|
72
|
+
fn.call(currentTarget, delegated[1], event);
|
|
73
|
+
} else if (len === 3) {
|
|
74
|
+
fn.call(currentTarget, delegated[1], delegated[2], event);
|
|
75
|
+
} else if (len === 4) {
|
|
76
|
+
fn.call(currentTarget, delegated[1], delegated[2], delegated[3], event);
|
|
77
|
+
} else {
|
|
78
|
+
// Fallback for rare cases with many args
|
|
79
|
+
const args = [];
|
|
80
|
+
for (let i = 1; i < len; i++) args.push(delegated[i]);
|
|
81
|
+
args.push(event);
|
|
82
|
+
fn.apply(currentTarget, args);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Simple form: a function
|
|
86
|
+
delegated.call(currentTarget, event);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Don't let handler errors break propagation - report async
|
|
91
|
+
queueMicrotask(() => {
|
|
92
|
+
throw error;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Stop if propagation was cancelled or we've reached the root
|
|
96
|
+
if (event.cancelBubble || parentElement === handlerElement || parentElement === null) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
currentTarget = parentElement;
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
event.__root = handlerElement;
|
|
103
|
+
delete event.currentTarget;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Register event types for delegation. Called by compiled code.
|
|
109
|
+
* e.g., delegate(["click", "input"]) at the end of a component file.
|
|
110
|
+
*/
|
|
111
|
+
export function delegate(events) {
|
|
112
|
+
for (let i = 0; i < events.length; i++) {
|
|
113
|
+
allRegisteredEvents.add(events[i]);
|
|
114
|
+
}
|
|
115
|
+
// Notify all registered roots (document + any shadow roots) about new events
|
|
116
|
+
for (const fn of rootEventHandles) {
|
|
117
|
+
fn(events);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Track which roots already have delegation set up
|
|
122
|
+
const handledRoots = new WeakSet();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Set up event delegation on a root element (document or shadow root).
|
|
126
|
+
* Called once per root. Returns a cleanup function.
|
|
127
|
+
*/
|
|
128
|
+
export function handleRootEvents(target) {
|
|
129
|
+
if (handledRoots.has(target)) return;
|
|
130
|
+
handledRoots.add(target);
|
|
131
|
+
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const registeredEvents = new Set();
|
|
134
|
+
|
|
135
|
+
// Handler to add listeners for new event types
|
|
136
|
+
const eventHandle = (events) => {
|
|
137
|
+
for (const eventName of events) {
|
|
138
|
+
if (registeredEvents.has(eventName)) continue;
|
|
139
|
+
registeredEvents.add(eventName);
|
|
140
|
+
target.addEventListener(eventName, handleEventPropagation, {
|
|
141
|
+
passive: PASSIVE_EVENTS.has(eventName),
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Register for already-known events and future ones
|
|
148
|
+
eventHandle(allRegisteredEvents);
|
|
149
|
+
rootEventHandles.add(eventHandle);
|
|
150
|
+
|
|
151
|
+
// Cleanup: Abort removes all listeners, then unregister from future events
|
|
152
|
+
return () => {
|
|
153
|
+
controller.abort();
|
|
154
|
+
rootEventHandles.delete(eventHandle);
|
|
155
|
+
};
|
|
156
|
+
}
|