@relicloops/cathode 2.0.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.
- package/LICENSE +201 -0
- package/README.md +390 -0
- package/index.js +9 -0
- package/jsx-dev-runtime.js +1 -0
- package/jsx-runtime.js +20 -0
- package/lib/css.js +40 -0
- package/lib/eject.js +3 -0
- package/lib/error-boundary.js +10 -0
- package/lib/inject.js +27 -0
- package/lib/lazy-helpers.js +98 -0
- package/lib/lazy.js +96 -0
- package/lib/mount.js +17 -0
- package/lib/runtime.js +188 -0
- package/lib/suspense.js +40 -0
- package/package.json +35 -0
- package/types/index.d.ts +21 -0
- package/types/jsx-dev-runtime.d.ts +1 -0
- package/types/jsx-runtime.d.ts +17 -0
- package/types/lib/css.d.ts +4 -0
- package/types/lib/eject.d.ts +1 -0
- package/types/lib/error-boundary.d.ts +6 -0
- package/types/lib/inject.d.ts +5 -0
- package/types/lib/lazy-helpers.d.ts +61 -0
- package/types/lib/lazy.d.ts +20 -0
- package/types/lib/mount.d.ts +7 -0
- package/types/lib/runtime.d.ts +10 -0
- package/types/lib/suspense.d.ts +8 -0
package/lib/inject.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render } from './runtime.js';
|
|
2
|
+
function renderToFragment(node) {
|
|
3
|
+
const temp = document.createElement('div');
|
|
4
|
+
render(node, temp);
|
|
5
|
+
const fragment = document.createDocumentFragment();
|
|
6
|
+
while (temp.firstChild) {
|
|
7
|
+
fragment.appendChild(temp.firstChild);
|
|
8
|
+
}
|
|
9
|
+
return fragment;
|
|
10
|
+
}
|
|
11
|
+
export async function inject(node, parent, options) {
|
|
12
|
+
const mode = options?.mode ?? 'replace';
|
|
13
|
+
if (mode === 'replace') {
|
|
14
|
+
render(node, parent);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const fragment = renderToFragment(node);
|
|
18
|
+
if (mode === 'append') {
|
|
19
|
+
parent.appendChild(fragment);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (mode === 'prepend') {
|
|
23
|
+
parent.insertBefore(fragment, parent.firstChild);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
render(node, parent);
|
|
27
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns event props that trigger lazy loading on hover
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const Dashboard = lazyOnDemand(() => import('./Dashboard.js'));
|
|
6
|
+
* const hoverProps = lazyOnHover(Dashboard);
|
|
7
|
+
* <a href="/dashboard" {...hoverProps}>Dashboard</a>
|
|
8
|
+
*/
|
|
9
|
+
export function lazyOnHover(component) {
|
|
10
|
+
return {
|
|
11
|
+
onMouseEnter: () => {
|
|
12
|
+
// console.trace( '[lazyOnHover] Mouse enter, status:', component.__status );
|
|
13
|
+
if (component.__status === 'pending') {
|
|
14
|
+
component.awake();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Triggers lazy loading after a delay
|
|
21
|
+
*
|
|
22
|
+
* @param component - The lazy component to load
|
|
23
|
+
* @param delayMs - Delay in milliseconds before loading
|
|
24
|
+
* @returns Cleanup function to cancel the timeout
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const Analytics = lazyOnDemand(() => import('./Analytics.js'));
|
|
28
|
+
* lazyAfterDelay(Analytics, 5000); // Load after 5 seconds
|
|
29
|
+
*/
|
|
30
|
+
export function lazyAfterDelay(component, delayMs) {
|
|
31
|
+
// console.trace( '[lazyAfterDelay] Setting up delay:', delayMs, 'ms, status:', component.__status );
|
|
32
|
+
if (component.__status !== 'pending') {
|
|
33
|
+
return () => { };
|
|
34
|
+
}
|
|
35
|
+
const timeoutId = window.setTimeout(() => {
|
|
36
|
+
// console.trace( '[lazyAfterDelay] Delay elapsed, calling awake()' );
|
|
37
|
+
component.awake();
|
|
38
|
+
}, delayMs);
|
|
39
|
+
return () => clearTimeout(timeoutId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Triggers lazy loading when the browser is idle
|
|
43
|
+
*
|
|
44
|
+
* @param component - The lazy component to load
|
|
45
|
+
* @param timeout - Maximum time to wait before loading (default: 5000ms)
|
|
46
|
+
* @returns Cleanup function to cancel the idle callback
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const ChatWidget = lazyOnDemand(() => import('./ChatWidget.js'));
|
|
50
|
+
* lazyWhenIdle(ChatWidget); // Load when browser idle
|
|
51
|
+
*/
|
|
52
|
+
export function lazyWhenIdle(component, timeout = 5000) {
|
|
53
|
+
// console.trace( '[lazyWhenIdle] Setting up idle callback, timeout:', timeout, 'ms, status:', component.__status );
|
|
54
|
+
if (typeof requestIdleCallback === 'undefined') {
|
|
55
|
+
// console.trace( '[lazyWhenIdle] requestIdleCallback not available, using delay fallback' );
|
|
56
|
+
return lazyAfterDelay(component, timeout);
|
|
57
|
+
}
|
|
58
|
+
if (component.__status !== 'pending') {
|
|
59
|
+
return () => { };
|
|
60
|
+
}
|
|
61
|
+
const requestId = requestIdleCallback(() => {
|
|
62
|
+
// console.trace( '[lazyWhenIdle] Idle callback triggered, calling awake()' );
|
|
63
|
+
component.awake();
|
|
64
|
+
}, { timeout });
|
|
65
|
+
return () => cancelIdleCallback(requestId);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Component wrapper that triggers lazy loading when scrolled into view
|
|
69
|
+
*
|
|
70
|
+
* @param props.component - The lazy component to load
|
|
71
|
+
* @param props.componentProps - Props to pass to the component when loaded
|
|
72
|
+
* @param props.fallback - Content to show before component is visible
|
|
73
|
+
* @param props.rootMargin - Margin around root for intersection detection (e.g., "200px")
|
|
74
|
+
* @param props.threshold - Visibility threshold (0-1) that triggers loading
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const Footer = lazyOnDemand(() => import('./Footer.js'));
|
|
78
|
+
* <LazyOnVisible
|
|
79
|
+
* component={Footer}
|
|
80
|
+
* fallback={<div style="height: 200px">Scroll to load footer</div>}
|
|
81
|
+
* rootMargin="200px"
|
|
82
|
+
* />
|
|
83
|
+
*/
|
|
84
|
+
export function LazyOnVisible(props) {
|
|
85
|
+
return {
|
|
86
|
+
type: '__lazy_onvisible__',
|
|
87
|
+
props: {
|
|
88
|
+
component: props.component,
|
|
89
|
+
componentProps: props.componentProps || {},
|
|
90
|
+
fallback: props.fallback || null,
|
|
91
|
+
observerOptions: {
|
|
92
|
+
rootMargin: props.rootMargin,
|
|
93
|
+
threshold: props.threshold
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
children: []
|
|
97
|
+
};
|
|
98
|
+
}
|
package/lib/lazy.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { h } from './runtime.js';
|
|
2
|
+
const autoLoadCache = new Map();
|
|
3
|
+
const onDemandCache = new Map();
|
|
4
|
+
function createLazyComponent(loader, autoLoad) {
|
|
5
|
+
const cache = autoLoad ? autoLoadCache : onDemandCache;
|
|
6
|
+
let warnedDeprecatedLoad = false;
|
|
7
|
+
if (cache.has(loader)) {
|
|
8
|
+
return cache.get(loader);
|
|
9
|
+
}
|
|
10
|
+
const LazyWrapper = ((props) => {
|
|
11
|
+
// console.trace( '[LazyWrapper] Called, autoLoad:', autoLoad, 'status:', LazyWrapper.__status );
|
|
12
|
+
if (LazyWrapper.__status === 'resolved' && LazyWrapper.__component) {
|
|
13
|
+
// console.trace( '[LazyWrapper] Returning resolved component' );
|
|
14
|
+
return h(LazyWrapper.__component, props);
|
|
15
|
+
}
|
|
16
|
+
if (LazyWrapper.__status === 'rejected') {
|
|
17
|
+
// console.trace( '[LazyWrapper] Returning error' );
|
|
18
|
+
return {
|
|
19
|
+
type: '__lazy_error__',
|
|
20
|
+
props: { error: LazyWrapper.__error },
|
|
21
|
+
children: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!LazyWrapper.__promise && LazyWrapper.__autoLoad) {
|
|
25
|
+
// console.trace( '[LazyWrapper] Auto-loading' );
|
|
26
|
+
LazyWrapper.awake();
|
|
27
|
+
}
|
|
28
|
+
const vnode = {
|
|
29
|
+
type: LazyWrapper.__autoLoad ? '__lazy_pending__' : '__lazy_ondemand_pending__',
|
|
30
|
+
props: { wrapper: LazyWrapper, componentProps: props },
|
|
31
|
+
children: []
|
|
32
|
+
};
|
|
33
|
+
// console.trace( '[LazyWrapper] Returning VNode with type:', vnode.type );
|
|
34
|
+
return vnode;
|
|
35
|
+
});
|
|
36
|
+
LazyWrapper.__lazy = true;
|
|
37
|
+
LazyWrapper.__status = 'pending';
|
|
38
|
+
LazyWrapper.__component = null;
|
|
39
|
+
LazyWrapper.__error = null;
|
|
40
|
+
LazyWrapper.__promise = null;
|
|
41
|
+
LazyWrapper.__autoLoad = autoLoad;
|
|
42
|
+
LazyWrapper.awake = () => {
|
|
43
|
+
// console.trace( '[LazyWrapper.awake] Called, autoLoad:', autoLoad, 'has promise:', !!LazyWrapper.__promise );
|
|
44
|
+
if (LazyWrapper.__promise) {
|
|
45
|
+
// console.trace( '[LazyWrapper.awake] Promise already exists, returning it' );
|
|
46
|
+
return LazyWrapper.__promise;
|
|
47
|
+
}
|
|
48
|
+
// SSR safety
|
|
49
|
+
if (typeof document === 'undefined') {
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
}
|
|
52
|
+
// console.trace( '[LazyWrapper.awake] Starting loader()' );
|
|
53
|
+
LazyWrapper.__promise = loader()
|
|
54
|
+
.then(mod => {
|
|
55
|
+
let component = null;
|
|
56
|
+
if (mod && typeof mod === 'object' && 'default' in mod && mod.default) {
|
|
57
|
+
component = mod.default;
|
|
58
|
+
}
|
|
59
|
+
else if (typeof mod === 'function') {
|
|
60
|
+
component = mod;
|
|
61
|
+
}
|
|
62
|
+
else if (mod && typeof mod === 'object') {
|
|
63
|
+
const candidates = Object.values(mod).filter((value) => typeof value === 'function');
|
|
64
|
+
if (candidates.length === 1) {
|
|
65
|
+
component = candidates[0];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!component) {
|
|
69
|
+
const keys = mod && typeof mod === 'object' ? Object.keys(mod) : [];
|
|
70
|
+
throw new Error(`Cathode lazy loader: missing component export. Provide a default export or a single named export. Found: [${keys.join(', ')}]`);
|
|
71
|
+
}
|
|
72
|
+
LazyWrapper.__component = component;
|
|
73
|
+
LazyWrapper.__status = 'resolved';
|
|
74
|
+
})
|
|
75
|
+
.catch(err => {
|
|
76
|
+
LazyWrapper.__error = err;
|
|
77
|
+
LazyWrapper.__status = 'rejected';
|
|
78
|
+
});
|
|
79
|
+
return LazyWrapper.__promise;
|
|
80
|
+
};
|
|
81
|
+
LazyWrapper.__load = () => {
|
|
82
|
+
if (!warnedDeprecatedLoad && typeof console !== 'undefined') {
|
|
83
|
+
console.warn('[Cathode] Lazy component __load() is deprecated. Use awake() instead.');
|
|
84
|
+
warnedDeprecatedLoad = true;
|
|
85
|
+
}
|
|
86
|
+
return LazyWrapper.awake();
|
|
87
|
+
};
|
|
88
|
+
cache.set(loader, LazyWrapper);
|
|
89
|
+
return LazyWrapper;
|
|
90
|
+
}
|
|
91
|
+
export function lazy(loader) {
|
|
92
|
+
return createLazyComponent(loader, true);
|
|
93
|
+
}
|
|
94
|
+
export function lazyOnDemand(loader) {
|
|
95
|
+
return createLazyComponent(loader, false);
|
|
96
|
+
}
|
package/lib/mount.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render } from './runtime.js';
|
|
2
|
+
const mountedParents = new WeakSet();
|
|
3
|
+
export async function mount(node, parent) {
|
|
4
|
+
render(node, parent);
|
|
5
|
+
mountedParents.add(parent);
|
|
6
|
+
return {
|
|
7
|
+
unmount: async () => unmount(parent),
|
|
8
|
+
cleanup: async () => cleanup(parent),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export async function unmount(parent) {
|
|
12
|
+
parent.innerHTML = '';
|
|
13
|
+
mountedParents.delete(parent);
|
|
14
|
+
}
|
|
15
|
+
export async function cleanup(parent) {
|
|
16
|
+
await unmount(parent);
|
|
17
|
+
}
|
package/lib/runtime.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { findLazyPending } from './suspense.js';
|
|
2
|
+
export function h(type, props, ...children) {
|
|
3
|
+
return { type, props: props || {}, children };
|
|
4
|
+
}
|
|
5
|
+
export const Fragment = (props) => Array.isArray(props.children) ? props.children : [props.children];
|
|
6
|
+
export function render(node, parent) {
|
|
7
|
+
parent.innerHTML = '';
|
|
8
|
+
parent.appendChild(toDOM(node));
|
|
9
|
+
}
|
|
10
|
+
function toDOM(node) {
|
|
11
|
+
if (node === null || node === undefined) {
|
|
12
|
+
return document.createTextNode('');
|
|
13
|
+
}
|
|
14
|
+
if (typeof node === 'string' || typeof node === 'number') {
|
|
15
|
+
return document.createTextNode(String(node));
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(node)) {
|
|
18
|
+
const frag = document.createDocumentFragment();
|
|
19
|
+
node.forEach(child => frag.appendChild(toDOM(child)));
|
|
20
|
+
return frag;
|
|
21
|
+
}
|
|
22
|
+
// Debug: log VNode type
|
|
23
|
+
if (node.type === '__lazy_ondemand_pending__') {
|
|
24
|
+
// console.trace( '[DEBUG] toDOM processing __lazy_ondemand_pending__' );
|
|
25
|
+
}
|
|
26
|
+
// Handle Suspense boundary
|
|
27
|
+
if (node.type === '__suspense__') {
|
|
28
|
+
const { fallback, children } = node.props;
|
|
29
|
+
const pending = findLazyPending(children);
|
|
30
|
+
if (pending.length === 0) {
|
|
31
|
+
return toDOM(children);
|
|
32
|
+
}
|
|
33
|
+
const container = document.createElement('div');
|
|
34
|
+
container.setAttribute('data-cathode-suspense', 'true');
|
|
35
|
+
container.appendChild(toDOM(fallback));
|
|
36
|
+
Promise.all(pending.map(lc => lc.awake())).then(() => {
|
|
37
|
+
const content = toDOM(children);
|
|
38
|
+
container.innerHTML = '';
|
|
39
|
+
container.appendChild(content);
|
|
40
|
+
});
|
|
41
|
+
return container;
|
|
42
|
+
}
|
|
43
|
+
// Handle lazy pending marker
|
|
44
|
+
if (node.type === '__lazy_pending__') {
|
|
45
|
+
const { wrapper, componentProps } = node.props;
|
|
46
|
+
const placeholder = document.createElement('div');
|
|
47
|
+
placeholder.setAttribute('data-cathode-lazy', 'pending');
|
|
48
|
+
wrapper.awake().then(() => {
|
|
49
|
+
if (wrapper.__status === 'resolved') {
|
|
50
|
+
const resolved = toDOM(h(wrapper.__component, componentProps));
|
|
51
|
+
placeholder.replaceWith(resolved);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return placeholder;
|
|
55
|
+
}
|
|
56
|
+
// Handle lazy ondemand pending marker
|
|
57
|
+
if (node.type === '__lazy_ondemand_pending__') {
|
|
58
|
+
const { wrapper, componentProps } = node.props;
|
|
59
|
+
const placeholder = document.createElement('div');
|
|
60
|
+
placeholder.setAttribute('data-cathode-lazy', 'ondemand-pending');
|
|
61
|
+
placeholder.style.display = 'none';
|
|
62
|
+
// Store reference for manual re-render after load
|
|
63
|
+
placeholder.__lazyComponent = wrapper;
|
|
64
|
+
placeholder.__componentProps = componentProps;
|
|
65
|
+
// Set up a mechanism to watch for when awake() is called
|
|
66
|
+
// console.trace( '[lazy ondemand] Created placeholder, status:', wrapper.__status, 'has promise:', !!wrapper.__promise );
|
|
67
|
+
let stopPolling = false;
|
|
68
|
+
let pollCount = 0;
|
|
69
|
+
const checkAndRender = () => {
|
|
70
|
+
pollCount++;
|
|
71
|
+
// Stop if placeholder is no longer in the document
|
|
72
|
+
if (stopPolling) {
|
|
73
|
+
// console.trace( '[lazy ondemand] Stopped polling, stopPolling:', stopPolling );
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Wait for element to be connected before checking
|
|
77
|
+
if (!placeholder.isConnected) {
|
|
78
|
+
if (pollCount % 10 === 0) {
|
|
79
|
+
// console.trace( '[lazy ondemand] Waiting for connection...', pollCount );
|
|
80
|
+
}
|
|
81
|
+
setTimeout(checkAndRender, 100);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (wrapper.__promise) {
|
|
85
|
+
// console.trace( '[lazy ondemand] Found promise after', pollCount, 'polls, status:', wrapper.__status );
|
|
86
|
+
stopPolling = true; // Stop polling once we find the promise
|
|
87
|
+
wrapper.__promise.then(() => {
|
|
88
|
+
// console.trace( '[lazy ondemand] Component loaded:', wrapper.__component?.name, 'status:', wrapper.__status, 'connected:', placeholder.isConnected );
|
|
89
|
+
if (wrapper.__status === 'resolved' && placeholder.isConnected) {
|
|
90
|
+
const resolved = toDOM(h(wrapper.__component, componentProps));
|
|
91
|
+
// console.trace( '[lazy ondemand] Replacing placeholder with resolved component' );
|
|
92
|
+
placeholder.replaceWith(resolved);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (pollCount % 10 === 0) {
|
|
98
|
+
// console.trace( '[lazy ondemand] Still polling...', pollCount, 'status:', wrapper.__status );
|
|
99
|
+
}
|
|
100
|
+
// Check again after a short delay
|
|
101
|
+
setTimeout(checkAndRender, 100);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
// Start checking after a small delay to allow DOM connection
|
|
105
|
+
setTimeout(checkAndRender, 0);
|
|
106
|
+
return placeholder;
|
|
107
|
+
}
|
|
108
|
+
// Handle lazy onvisible marker
|
|
109
|
+
if (node.type === '__lazy_onvisible__') {
|
|
110
|
+
const { component, componentProps, fallback, observerOptions } = node.props;
|
|
111
|
+
const container = document.createElement('div');
|
|
112
|
+
container.setAttribute('data-cathode-lazy', 'onvisible');
|
|
113
|
+
if (fallback) {
|
|
114
|
+
container.appendChild(toDOM(fallback));
|
|
115
|
+
}
|
|
116
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
117
|
+
const observer = new IntersectionObserver((entries) => {
|
|
118
|
+
entries.forEach((entry) => {
|
|
119
|
+
if (entry.isIntersecting) {
|
|
120
|
+
component.awake().then(() => {
|
|
121
|
+
if (component.__status === 'resolved') {
|
|
122
|
+
const resolved = toDOM(h(component, componentProps));
|
|
123
|
+
container.innerHTML = '';
|
|
124
|
+
container.appendChild(resolved);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
observer.disconnect();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}, observerOptions);
|
|
131
|
+
observer.observe(container);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// SSR or old browser fallback
|
|
135
|
+
component.awake();
|
|
136
|
+
}
|
|
137
|
+
return container;
|
|
138
|
+
}
|
|
139
|
+
// Handle lazy error marker
|
|
140
|
+
if (node.type === '__lazy_error__') {
|
|
141
|
+
const errorDiv = document.createElement('div');
|
|
142
|
+
errorDiv.setAttribute('data-cathode-lazy', 'error');
|
|
143
|
+
errorDiv.textContent = `Error: ${node.props.error?.message || 'Unknown'}`;
|
|
144
|
+
return errorDiv;
|
|
145
|
+
}
|
|
146
|
+
// Handle error boundary
|
|
147
|
+
if (node.type === '__error_boundary__') {
|
|
148
|
+
const { fallback, children } = node.props;
|
|
149
|
+
try {
|
|
150
|
+
return toDOM(children);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (typeof fallback === 'function') {
|
|
154
|
+
return toDOM(fallback(error));
|
|
155
|
+
}
|
|
156
|
+
return toDOM(fallback);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (typeof node.type === 'function') {
|
|
160
|
+
return toDOM(node.type({ ...node.props, children: node.children }));
|
|
161
|
+
}
|
|
162
|
+
const el = document.createElement(node.type);
|
|
163
|
+
const props = node.props || {};
|
|
164
|
+
Object.entries(props).forEach(([key, value]) => {
|
|
165
|
+
if (key === 'className') {
|
|
166
|
+
if (typeof value === 'string') {
|
|
167
|
+
el.setAttribute('class', value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (key.startsWith('on') && typeof value === 'function') {
|
|
171
|
+
el[key.toLowerCase()] = value;
|
|
172
|
+
}
|
|
173
|
+
else if (value !== false && value !== null) {
|
|
174
|
+
el.setAttribute(key, String(value));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (node.children) {
|
|
178
|
+
node.children.forEach((child) => {
|
|
179
|
+
el.appendChild(toDOM(child));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return el;
|
|
183
|
+
}
|
|
184
|
+
// Expose globals for JSX pragma usage when bundled.
|
|
185
|
+
if (typeof globalThis !== 'undefined') {
|
|
186
|
+
globalThis.h = h;
|
|
187
|
+
globalThis.Fragment = Fragment;
|
|
188
|
+
}
|
package/lib/suspense.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function Suspense(props) {
|
|
2
|
+
return {
|
|
3
|
+
type: '__suspense__',
|
|
4
|
+
props: {
|
|
5
|
+
fallback: props.fallback,
|
|
6
|
+
children: props.children
|
|
7
|
+
},
|
|
8
|
+
children: Array.isArray(props.children) ? props.children : [props.children]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function findLazyPending(node) {
|
|
12
|
+
if (!node || typeof node === 'string' || typeof node === 'number') {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(node)) {
|
|
16
|
+
return node.flatMap(n => findLazyPending(n));
|
|
17
|
+
}
|
|
18
|
+
const vnode = node;
|
|
19
|
+
// Check if type is a lazy component function (has __lazy marker)
|
|
20
|
+
if (typeof vnode.type === 'function' && vnode.type.__lazy) {
|
|
21
|
+
const lazyComp = vnode.type;
|
|
22
|
+
// Only return if still pending AND (autoLoad OR already manually loading)
|
|
23
|
+
if (lazyComp.__status === 'pending' && (lazyComp.__autoLoad || lazyComp.__promise)) {
|
|
24
|
+
return [lazyComp];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Also check for already-rendered lazy pending marker
|
|
28
|
+
if (vnode.type === '__lazy_pending__') {
|
|
29
|
+
return [vnode.props.wrapper];
|
|
30
|
+
}
|
|
31
|
+
// Skip ondemand pending markers (not auto-loading)
|
|
32
|
+
if (vnode.type === '__lazy_ondemand_pending__') {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
// Recurse into children
|
|
36
|
+
if (vnode.children) {
|
|
37
|
+
return vnode.children.flatMap(c => findLazyPending(c));
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@relicloops/cathode",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Lightweight JSX runtime.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"types": "./types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./types/index.d.ts",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"./jsx-runtime": {
|
|
15
|
+
"types": "./types/jsx-runtime.d.ts",
|
|
16
|
+
"default": "./jsx-runtime.js"
|
|
17
|
+
},
|
|
18
|
+
"./jsx-dev-runtime": {
|
|
19
|
+
"types": "./types/jsx-dev-runtime.d.ts",
|
|
20
|
+
"default": "./jsx-dev-runtime.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"jsx-runtime.js",
|
|
26
|
+
"jsx-dev-runtime.js",
|
|
27
|
+
"lib/**/*.js",
|
|
28
|
+
"types/**/*.d.ts",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export * from './lib/runtime.js';
|
|
2
|
+
export { inject } from './lib/inject.js';
|
|
3
|
+
export type { InjectMode, InjectOptions } from './lib/inject.js';
|
|
4
|
+
export { eject } from './lib/eject.js';
|
|
5
|
+
export { mount, unmount, cleanup } from './lib/mount.js';
|
|
6
|
+
export type { MountHandle } from './lib/mount.js';
|
|
7
|
+
export { css } from './lib/css.js';
|
|
8
|
+
export type { CSSOptions } from './lib/css.js';
|
|
9
|
+
export { lazy, lazyOnDemand } from './lib/lazy.js';
|
|
10
|
+
export type { ComponentType, LazyComponent } from './lib/lazy.js';
|
|
11
|
+
export { lazyOnHover, lazyAfterDelay, lazyWhenIdle, LazyOnVisible } from './lib/lazy-helpers.js';
|
|
12
|
+
export { Suspense } from './lib/suspense.js';
|
|
13
|
+
export type { SuspenseProps } from './lib/suspense.js';
|
|
14
|
+
export { ErrorBoundary } from './lib/error-boundary.js';
|
|
15
|
+
export type { ErrorBoundaryProps } from './lib/error-boundary.js';
|
|
16
|
+
declare global {
|
|
17
|
+
namespace JSX {
|
|
18
|
+
interface IntrinsicElements extends Record<string, any> {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jsx-runtime.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VNode } from './lib/runtime.js';
|
|
2
|
+
import { Fragment } from './lib/runtime.js';
|
|
3
|
+
export { Fragment };
|
|
4
|
+
export type { VNode };
|
|
5
|
+
type JSXProps = Record<string, unknown> & {
|
|
6
|
+
children?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export declare namespace JSX {
|
|
9
|
+
interface Element extends VNode {
|
|
10
|
+
}
|
|
11
|
+
interface IntrinsicElements extends Record<string, any> {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export type JSXIntrinsicElements = JSX.IntrinsicElements;
|
|
15
|
+
export declare function jsx(type: VNode['type'], props: JSXProps | null, key?: unknown): JSX.Element;
|
|
16
|
+
export declare const jsxs: typeof jsx;
|
|
17
|
+
export declare const jsxDEV: typeof jsx;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function eject(parent: HTMLElement): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LazyComponent } from './lazy.js';
|
|
2
|
+
import type { VNode } from './runtime.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns event props that trigger lazy loading on hover
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const Dashboard = lazyOnDemand(() => import('./Dashboard.js'));
|
|
8
|
+
* const hoverProps = lazyOnHover(Dashboard);
|
|
9
|
+
* <a href="/dashboard" {...hoverProps}>Dashboard</a>
|
|
10
|
+
*/
|
|
11
|
+
export declare function lazyOnHover<P = any>(component: LazyComponent<P>): {
|
|
12
|
+
onMouseEnter: () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Triggers lazy loading after a delay
|
|
16
|
+
*
|
|
17
|
+
* @param component - The lazy component to load
|
|
18
|
+
* @param delayMs - Delay in milliseconds before loading
|
|
19
|
+
* @returns Cleanup function to cancel the timeout
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const Analytics = lazyOnDemand(() => import('./Analytics.js'));
|
|
23
|
+
* lazyAfterDelay(Analytics, 5000); // Load after 5 seconds
|
|
24
|
+
*/
|
|
25
|
+
export declare function lazyAfterDelay<P = any>(component: LazyComponent<P>, delayMs: number): () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Triggers lazy loading when the browser is idle
|
|
28
|
+
*
|
|
29
|
+
* @param component - The lazy component to load
|
|
30
|
+
* @param timeout - Maximum time to wait before loading (default: 5000ms)
|
|
31
|
+
* @returns Cleanup function to cancel the idle callback
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const ChatWidget = lazyOnDemand(() => import('./ChatWidget.js'));
|
|
35
|
+
* lazyWhenIdle(ChatWidget); // Load when browser idle
|
|
36
|
+
*/
|
|
37
|
+
export declare function lazyWhenIdle<P = any>(component: LazyComponent<P>, timeout?: number): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Component wrapper that triggers lazy loading when scrolled into view
|
|
40
|
+
*
|
|
41
|
+
* @param props.component - The lazy component to load
|
|
42
|
+
* @param props.componentProps - Props to pass to the component when loaded
|
|
43
|
+
* @param props.fallback - Content to show before component is visible
|
|
44
|
+
* @param props.rootMargin - Margin around root for intersection detection (e.g., "200px")
|
|
45
|
+
* @param props.threshold - Visibility threshold (0-1) that triggers loading
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const Footer = lazyOnDemand(() => import('./Footer.js'));
|
|
49
|
+
* <LazyOnVisible
|
|
50
|
+
* component={Footer}
|
|
51
|
+
* fallback={<div style="height: 200px">Scroll to load footer</div>}
|
|
52
|
+
* rootMargin="200px"
|
|
53
|
+
* />
|
|
54
|
+
*/
|
|
55
|
+
export declare function LazyOnVisible<P = any>(props: {
|
|
56
|
+
component: LazyComponent<P>;
|
|
57
|
+
componentProps?: P;
|
|
58
|
+
fallback?: VNode | string;
|
|
59
|
+
rootMargin?: string;
|
|
60
|
+
threshold?: number;
|
|
61
|
+
}): VNode;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { VNode } from './runtime.js';
|
|
2
|
+
export type ComponentType<P = any> = (props: P) => VNode | VNode[] | string | number | null | undefined;
|
|
3
|
+
type LazyLoader<P = any> = () => Promise<{
|
|
4
|
+
default: ComponentType<P>;
|
|
5
|
+
}>;
|
|
6
|
+
export interface LazyComponent<P = any> {
|
|
7
|
+
(props: P): VNode;
|
|
8
|
+
__lazy: true;
|
|
9
|
+
__status: 'pending' | 'resolved' | 'rejected';
|
|
10
|
+
__component: ComponentType<P> | null;
|
|
11
|
+
__error: Error | null;
|
|
12
|
+
__promise: Promise<void> | null;
|
|
13
|
+
/** @deprecated Use awake() instead. */
|
|
14
|
+
__load: () => Promise<void>;
|
|
15
|
+
awake: () => Promise<void>;
|
|
16
|
+
__autoLoad: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function lazy<P = any>(loader: LazyLoader<P>): LazyComponent<P>;
|
|
19
|
+
export declare function lazyOnDemand<P = any>(loader: LazyLoader<P>): LazyComponent<P>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type MountHandle = {
|
|
2
|
+
unmount: () => Promise<void>;
|
|
3
|
+
cleanup: () => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
export declare function mount(node: any, parent: HTMLElement): Promise<MountHandle>;
|
|
6
|
+
export declare function unmount(parent: HTMLElement): Promise<void>;
|
|
7
|
+
export declare function cleanup(parent: HTMLElement): Promise<void>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type VNode = {
|
|
2
|
+
type: string | ((props: any) => VNode | VNode[] | string | number | null | undefined);
|
|
3
|
+
props: Record<string, any>;
|
|
4
|
+
children: Array<VNode | string | number | null | undefined>;
|
|
5
|
+
};
|
|
6
|
+
export declare function h(type: VNode['type'], props: Record<string, any> | null, ...children: Array<VNode | string | number | null | undefined>): VNode;
|
|
7
|
+
export declare const Fragment: (props: {
|
|
8
|
+
children?: any;
|
|
9
|
+
}) => any[];
|
|
10
|
+
export declare function render(node: any, parent: HTMLElement): void;
|