@servlyadmin/runtime-core 0.1.36 → 0.1.38

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/README.md CHANGED
@@ -17,8 +17,8 @@ pnpm add @servlyadmin/runtime-core
17
17
  ```typescript
18
18
  import { render, fetchComponent } from '@servlyadmin/runtime-core';
19
19
 
20
- // Fetch a component from the registry
21
- const { data } = await fetchComponent('my-component', { version: 'latest' });
20
+ // Fetch a component from the registry (uses Servly's default registry)
21
+ const { data } = await fetchComponent('my-component-id');
22
22
 
23
23
  // Render to a container
24
24
  const result = render({
@@ -42,6 +42,47 @@ result.update({
42
42
  result.destroy();
43
43
  ```
44
44
 
45
+ ## Registry Configuration
46
+
47
+ By default, components are fetched from Servly's production registry:
48
+ - **Default URL**: `https://core-api.servly.app/v1/views/registry`
49
+
50
+ You can override this if you have a custom registry:
51
+
52
+ ```typescript
53
+ import { setRegistryUrl } from '@servlyadmin/runtime-core';
54
+
55
+ // Use a custom registry
56
+ setRegistryUrl('https://your-api.com/v1/views/registry');
57
+ ```
58
+
59
+ ## Cache Strategies
60
+
61
+ The runtime supports three caching strategies to optimize component loading:
62
+
63
+ | Strategy | Description | Persistence | Best For |
64
+ |----------|-------------|-------------|----------|
65
+ | `localStorage` | Persists across browser sessions | Yes | Production apps (default) |
66
+ | `memory` | In-memory cache, cleared on page refresh | No | Development, SSR |
67
+ | `none` | No caching, always fetches fresh | No | Testing, debugging |
68
+
69
+ **Default**: `localStorage` - Components are cached in the browser's localStorage for fast subsequent loads.
70
+
71
+ ```typescript
72
+ // Use default localStorage caching
73
+ const { data } = await fetchComponent('my-component');
74
+
75
+ // Explicitly set cache strategy
76
+ const { data } = await fetchComponent('my-component', {
77
+ cacheStrategy: 'memory', // or 'localStorage' or 'none'
78
+ });
79
+
80
+ // Force refresh (bypass cache)
81
+ const { data } = await fetchComponent('my-component', {
82
+ forceRefresh: true,
83
+ });
84
+ ```
85
+
45
86
  ## Core Concepts
46
87
 
47
88
  ### Layout Elements
@@ -51,14 +92,15 @@ Components are defined as a tree of layout elements:
51
92
  ```typescript
52
93
  interface LayoutElement {
53
94
  i: string; // Unique identifier
54
- type: string; // HTML tag or component type
95
+ componentId: string; // Element type (container, text, button, etc.)
55
96
  configuration?: { // Element configuration
56
- className?: string;
97
+ classNames?: string;
57
98
  style?: Record<string, any>;
58
- textContent?: string;
99
+ text?: string;
59
100
  // ... other attributes
60
101
  };
61
- children?: LayoutElement[]; // Nested elements
102
+ children?: string[]; // Child element IDs
103
+ parent?: string; // Parent element ID
62
104
  }
63
105
  ```
64
106
 
@@ -82,10 +124,10 @@ Use `{{path}}` syntax to bind data:
82
124
  const elements = [
83
125
  {
84
126
  i: 'greeting',
85
- type: 'h1',
127
+ componentId: 'text',
86
128
  configuration: {
87
- textContent: 'Hello, {{props.name}}!',
88
- className: '{{props.className}}',
129
+ text: 'Hello, {{props.name}}!',
130
+ classNames: '{{props.className}}',
89
131
  },
90
132
  },
91
133
  ];
@@ -118,18 +160,33 @@ interface RenderResult {
118
160
  Fetches a component from the registry.
119
161
 
120
162
  ```typescript
121
- const { data, fromCache } = await fetchComponent('component-id', {
122
- version: 'latest', // Version specifier
123
- cacheStrategy: 'memory', // 'memory' | 'localStorage' | 'none'
124
- forceRefresh: false, // Bypass cache
125
- timeout: 30000, // Request timeout in ms
163
+ const { data, fromCache, version } = await fetchComponent('component-id', {
164
+ version: 'latest', // Version specifier (default: 'latest')
165
+ cacheStrategy: 'localStorage', // 'localStorage' | 'memory' | 'none' (default: 'localStorage')
166
+ forceRefresh: false, // Bypass cache (default: false)
126
167
  retryConfig: {
127
- maxRetries: 3,
128
- retryDelay: 1000,
168
+ maxRetries: 3, // Number of retry attempts (default: 3)
169
+ initialDelay: 1000, // Initial retry delay in ms (default: 1000)
170
+ maxDelay: 10000, // Maximum retry delay in ms (default: 10000)
171
+ backoffMultiplier: 2, // Exponential backoff multiplier (default: 2)
129
172
  },
130
173
  });
131
174
  ```
132
175
 
176
+ ### setRegistryUrl(url)
177
+
178
+ Configure a custom registry URL.
179
+
180
+ ```typescript
181
+ import { setRegistryUrl, DEFAULT_REGISTRY_URL } from '@servlyadmin/runtime-core';
182
+
183
+ // Use custom registry
184
+ setRegistryUrl('https://your-api.com/v1/views/registry');
185
+
186
+ // Reset to default
187
+ setRegistryUrl(DEFAULT_REGISTRY_URL);
188
+ ```
189
+
133
190
  ### StateManager
134
191
 
135
192
  Manages component state with subscriptions.
@@ -140,25 +197,17 @@ import { StateManager } from '@servlyadmin/runtime-core';
140
197
  const stateManager = new StateManager({ count: 0 });
141
198
 
142
199
  // Get/set state
143
- stateManager.setState({ count: 1 });
144
- stateManager.getValue('count'); // 1
145
- stateManager.setValue('user.name', 'John');
200
+ stateManager.set('count', 1);
201
+ stateManager.get('count'); // 1
202
+ stateManager.set('user.name', 'John');
146
203
 
147
204
  // Subscribe to changes
148
- const unsubscribe = stateManager.subscribe((state) => {
149
- console.log('State changed:', state);
205
+ const unsubscribe = stateManager.subscribe((event) => {
206
+ console.log('State changed:', event.path, event.value);
150
207
  });
151
208
 
152
- // Subscribe to specific path
153
- stateManager.subscribeToPath('count', (value) => {
154
- console.log('Count changed:', value);
155
- });
156
-
157
- // Batch updates
158
- stateManager.batch(() => {
159
- stateManager.setValue('a', 1);
160
- stateManager.setValue('b', 2);
161
- }); // Only one notification
209
+ // Cleanup
210
+ stateManager.clear();
162
211
  ```
163
212
 
164
213
  ### EventSystem
@@ -166,59 +215,52 @@ stateManager.batch(() => {
166
215
  Handles events with plugin-based actions.
167
216
 
168
217
  ```typescript
169
- import { EventSystem } from '@servlyadmin/runtime-core';
218
+ import { EventSystem, getEventSystem } from '@servlyadmin/runtime-core';
170
219
 
171
- const eventSystem = new EventSystem();
220
+ const eventSystem = getEventSystem();
172
221
 
173
222
  // Register custom plugin
174
- eventSystem.registerPlugin('my-action', async (config, context) => {
175
- console.log('Action executed with:', config);
176
- });
177
-
178
- // Create event handler
179
- const handler = eventSystem.createHandler([
180
- { key: 'prevent-default', config: {} },
181
- { key: 'set-state', config: { path: 'clicked', value: true } },
182
- { key: 'my-action', config: { message: 'Button clicked!' } },
183
- ]);
184
-
185
- // Use with element
186
- button.addEventListener('click', (e) => {
187
- handler(e, 'button-id', bindingContext);
223
+ eventSystem.registerPlugin('my-action', async (action, context) => {
224
+ console.log('Action executed with:', action.config);
188
225
  });
189
226
  ```
190
227
 
191
228
  #### Built-in Plugins
192
229
 
193
- - `console-log` - Log messages to console
194
- - `set-state` - Update state values
230
+ - `executeCode` - Execute arbitrary JavaScript code
231
+ - `state-setState` - Update state values
232
+ - `navigateTo` - Navigate to URL
233
+ - `localStorage-set/get/remove` - LocalStorage operations
234
+ - `sessionStorage-set/get` - SessionStorage operations
235
+ - `alert` - Show alert dialog
236
+ - `console-log` - Log to console
237
+ - `clipboard-copy` - Copy text to clipboard
238
+ - `scrollTo` - Scroll to element
239
+ - `focus/blur` - Focus/blur elements
240
+ - `addClass/removeClass/toggleClass` - CSS class manipulation
241
+ - `setAttribute/removeAttribute` - Attribute manipulation
242
+ - `dispatchEvent` - Dispatch custom events
195
243
  - `delay` - Add delay between actions
196
- - `prevent-default` - Call event.preventDefault()
197
- - `stop-propagation` - Call event.stopPropagation()
198
244
 
199
- ### Cache
200
-
201
- Component caching with multiple strategies.
245
+ ### Cache Management
202
246
 
203
247
  ```typescript
204
- import { ComponentCache } from '@servlyadmin/runtime-core';
205
-
206
- const cache = new ComponentCache({
207
- maxSize: 100,
208
- strategy: 'memory', // 'memory' | 'localStorage' | 'none'
209
- });
248
+ import {
249
+ clearAllCaches,
250
+ clearMemoryCache,
251
+ clearLocalStorageCache,
252
+ getMemoryCacheSize
253
+ } from '@servlyadmin/runtime-core';
210
254
 
211
- // Set with TTL
212
- cache.set('key', data, { ttl: 60000 }); // 1 minute
255
+ // Clear all caches
256
+ clearAllCaches();
213
257
 
214
- // Component-specific methods
215
- cache.setComponent('comp-id', '1.0.0', componentData);
216
- cache.getComponent('comp-id', 'latest');
217
- cache.invalidateComponent('comp-id');
258
+ // Clear specific cache
259
+ clearMemoryCache();
260
+ clearLocalStorageCache();
218
261
 
219
- // Statistics
220
- const stats = cache.getStats();
221
- console.log(`Hit rate: ${stats.hitRate * 100}%`);
262
+ // Get cache size
263
+ const size = getMemoryCacheSize();
222
264
  ```
223
265
 
224
266
  ### Bindings
@@ -226,7 +268,7 @@ console.log(`Hit rate: ${stats.hitRate * 100}%`);
226
268
  Template resolution utilities.
227
269
 
228
270
  ```typescript
229
- import { resolveTemplate, resolveBindings, isTemplate } from '@servlyadmin/runtime-core';
271
+ import { resolveTemplate, hasTemplateSyntax } from '@servlyadmin/runtime-core';
230
272
 
231
273
  const context = {
232
274
  props: { name: 'World', count: 42 },
@@ -237,16 +279,9 @@ const context = {
237
279
  // Resolve single template
238
280
  resolveTemplate('Hello, {{props.name}}!', context); // "Hello, World!"
239
281
 
240
- // Check if string is a template
241
- isTemplate('{{props.name}}'); // true
242
- isTemplate('static text'); // false
243
-
244
- // Resolve all bindings in an object
245
- resolveBindings({
246
- title: '{{props.name}}',
247
- subtitle: 'Count: {{props.count}}',
248
- }, context);
249
- // { title: 'World', subtitle: 'Count: 42' }
282
+ // Check if string has template syntax
283
+ hasTemplateSyntax('{{props.name}}'); // true
284
+ hasTemplateSyntax('static text'); // false
250
285
  ```
251
286
 
252
287
  ## Slots
@@ -257,24 +292,25 @@ Components can define slots for content injection:
257
292
  const elements = [
258
293
  {
259
294
  i: 'card',
260
- type: 'div',
261
- configuration: { className: 'card' },
262
- children: [
263
- {
264
- i: 'header-slot',
265
- type: 'div',
266
- configuration: {
267
- 'data-slot': 'header', // Slot placeholder
268
- },
269
- },
270
- {
271
- i: 'content-slot',
272
- type: 'div',
273
- configuration: {
274
- 'data-slot': 'default',
275
- },
276
- },
277
- ],
295
+ componentId: 'container',
296
+ configuration: { classNames: 'card' },
297
+ children: ['header-slot', 'content-slot'],
298
+ },
299
+ {
300
+ i: 'header-slot',
301
+ componentId: 'slot',
302
+ configuration: {
303
+ slotName: 'header',
304
+ },
305
+ parent: 'card',
306
+ },
307
+ {
308
+ i: 'content-slot',
309
+ componentId: 'slot',
310
+ configuration: {
311
+ slotName: 'default',
312
+ },
313
+ parent: 'card',
278
314
  },
279
315
  ];
280
316
  ```
@@ -290,9 +326,12 @@ import type {
290
326
  LayoutElement,
291
327
  BindingContext,
292
328
  RenderResult,
329
+ RenderOptions,
293
330
  ComponentData,
294
331
  CacheStrategy,
295
332
  RetryConfig,
333
+ FetchOptions,
334
+ FetchResult,
296
335
  } from '@servlyadmin/runtime-core';
297
336
  ```
298
337
 
@@ -0,0 +1,305 @@
1
+ // src/tailwind.ts
2
+ var DEFAULT_TAILWIND_CDN = "https://cdn.tailwindcss.com";
3
+ var TAILWIND_CACHE_KEY = "servly-tailwind-loaded";
4
+ var tailwindInjected = false;
5
+ var tailwindScript = null;
6
+ var foucStyleElement = null;
7
+ var tailwindReadyPromise = null;
8
+ var tailwindReadyResolve = null;
9
+ var FOUC_PREVENTION_CSS = `
10
+ .servly-component:not(.servly-ready),
11
+ [data-servly-id]:not(.servly-ready) {
12
+ visibility: hidden !important;
13
+ }
14
+ .servly-fouc-hidden {
15
+ visibility: hidden !important;
16
+ }
17
+ `;
18
+ function wasTailwindPreviouslyLoaded() {
19
+ if (typeof localStorage === "undefined") return false;
20
+ try {
21
+ return localStorage.getItem(TAILWIND_CACHE_KEY) === "true";
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function markTailwindAsLoaded() {
27
+ if (typeof localStorage === "undefined") return;
28
+ try {
29
+ localStorage.setItem(TAILWIND_CACHE_KEY, "true");
30
+ } catch {
31
+ }
32
+ }
33
+ function preloadTailwind(cdnUrl, usePlayCdn) {
34
+ if (typeof document === "undefined") return;
35
+ if (document.querySelector("link[data-servly-tailwind-preload]")) return;
36
+ const url = cdnUrl || DEFAULT_TAILWIND_CDN;
37
+ const fullUrl = usePlayCdn ? `${url}?plugins=forms,typography,aspect-ratio` : url;
38
+ const link = document.createElement("link");
39
+ link.rel = "preload";
40
+ link.as = "script";
41
+ link.href = fullUrl;
42
+ link.setAttribute("data-servly-tailwind-preload", "true");
43
+ if (document.head.firstChild) {
44
+ document.head.insertBefore(link, document.head.firstChild);
45
+ } else {
46
+ document.head.appendChild(link);
47
+ }
48
+ const dnsPrefetch = document.createElement("link");
49
+ dnsPrefetch.rel = "dns-prefetch";
50
+ dnsPrefetch.href = "https://cdn.tailwindcss.com";
51
+ document.head.appendChild(dnsPrefetch);
52
+ }
53
+ function preventFOUC() {
54
+ if (typeof document === "undefined") return;
55
+ if (foucStyleElement) return;
56
+ const wasLoaded = wasTailwindPreviouslyLoaded();
57
+ foucStyleElement = document.createElement("style");
58
+ foucStyleElement.id = "servly-fouc-prevention";
59
+ foucStyleElement.textContent = FOUC_PREVENTION_CSS;
60
+ if (document.head.firstChild) {
61
+ document.head.insertBefore(foucStyleElement, document.head.firstChild);
62
+ } else {
63
+ document.head.appendChild(foucStyleElement);
64
+ }
65
+ if (wasLoaded) {
66
+ setTimeout(() => {
67
+ if (window.tailwind) {
68
+ removeFOUCPrevention();
69
+ }
70
+ }, 100);
71
+ }
72
+ }
73
+ function removeFOUCPrevention() {
74
+ if (foucStyleElement && foucStyleElement.parentNode) {
75
+ foucStyleElement.parentNode.removeChild(foucStyleElement);
76
+ foucStyleElement = null;
77
+ }
78
+ if (typeof document !== "undefined") {
79
+ document.querySelectorAll(".servly-component, [data-servly-id]").forEach((el) => {
80
+ el.classList.add("servly-ready");
81
+ });
82
+ }
83
+ }
84
+ function markElementReady(element) {
85
+ element.classList.add("servly-ready");
86
+ element.classList.remove("servly-fouc-hidden");
87
+ }
88
+ function isTailwindReady() {
89
+ if (typeof window === "undefined") return false;
90
+ return tailwindInjected && !!window.tailwind;
91
+ }
92
+ function waitForTailwind() {
93
+ if (isTailwindReady()) {
94
+ return Promise.resolve();
95
+ }
96
+ if (tailwindReadyPromise) {
97
+ return tailwindReadyPromise;
98
+ }
99
+ tailwindReadyPromise = new Promise((resolve) => {
100
+ tailwindReadyResolve = resolve;
101
+ });
102
+ return tailwindReadyPromise;
103
+ }
104
+ function injectTailwind(config = {}) {
105
+ return new Promise((resolve, reject) => {
106
+ if (tailwindInjected && tailwindScript) {
107
+ resolve();
108
+ return;
109
+ }
110
+ if (typeof document === "undefined") {
111
+ resolve();
112
+ return;
113
+ }
114
+ const {
115
+ cdnUrl = DEFAULT_TAILWIND_CDN,
116
+ config: tailwindConfig,
117
+ plugins = [],
118
+ usePlayCdn = false,
119
+ onReady,
120
+ onError,
121
+ preventFOUC: shouldPreventFOUC = true,
122
+ enablePreload = true
123
+ } = config;
124
+ if (shouldPreventFOUC) {
125
+ preventFOUC();
126
+ }
127
+ if (enablePreload) {
128
+ preloadTailwind(cdnUrl, usePlayCdn);
129
+ }
130
+ if (window.tailwind) {
131
+ tailwindInjected = true;
132
+ markTailwindAsLoaded();
133
+ if (shouldPreventFOUC) {
134
+ removeFOUCPrevention();
135
+ }
136
+ if (tailwindReadyResolve) {
137
+ tailwindReadyResolve();
138
+ tailwindReadyResolve = null;
139
+ }
140
+ config.onReady?.();
141
+ resolve();
142
+ return;
143
+ }
144
+ const script = document.createElement("script");
145
+ script.src = usePlayCdn ? `${cdnUrl}?plugins=forms,typography,aspect-ratio` : cdnUrl;
146
+ script.async = true;
147
+ script.crossOrigin = "anonymous";
148
+ script.onload = () => {
149
+ tailwindInjected = true;
150
+ tailwindScript = script;
151
+ markTailwindAsLoaded();
152
+ if (tailwindConfig && window.tailwind) {
153
+ window.tailwind.config = tailwindConfig;
154
+ }
155
+ const delay = wasTailwindPreviouslyLoaded() ? 10 : 50;
156
+ setTimeout(() => {
157
+ if (shouldPreventFOUC) {
158
+ removeFOUCPrevention();
159
+ }
160
+ if (tailwindReadyResolve) {
161
+ tailwindReadyResolve();
162
+ tailwindReadyResolve = null;
163
+ }
164
+ onReady?.();
165
+ resolve();
166
+ }, delay);
167
+ };
168
+ script.onerror = (event) => {
169
+ const error = new Error(`Failed to load Tailwind CSS from ${cdnUrl}`);
170
+ if (shouldPreventFOUC) {
171
+ removeFOUCPrevention();
172
+ }
173
+ if (tailwindReadyResolve) {
174
+ tailwindReadyResolve();
175
+ tailwindReadyResolve = null;
176
+ }
177
+ onError?.(error);
178
+ reject(error);
179
+ };
180
+ document.head.appendChild(script);
181
+ });
182
+ }
183
+ function removeTailwind() {
184
+ if (tailwindScript && tailwindScript.parentNode) {
185
+ tailwindScript.parentNode.removeChild(tailwindScript);
186
+ tailwindScript = null;
187
+ tailwindInjected = false;
188
+ tailwindReadyPromise = null;
189
+ tailwindReadyResolve = null;
190
+ delete window.tailwind;
191
+ }
192
+ }
193
+ function isTailwindLoaded() {
194
+ return tailwindInjected || !!window.tailwind;
195
+ }
196
+ function getTailwind() {
197
+ return window.tailwind;
198
+ }
199
+ function updateTailwindConfig(config) {
200
+ if (window.tailwind) {
201
+ window.tailwind.config = {
202
+ ...window.tailwind.config,
203
+ ...config
204
+ };
205
+ }
206
+ }
207
+ function addCustomStyles(css, id) {
208
+ if (typeof document === "undefined") {
209
+ throw new Error("addCustomStyles can only be used in browser environment");
210
+ }
211
+ const styleId = id || `servly-custom-styles-${Date.now()}`;
212
+ let existingStyle = document.getElementById(styleId);
213
+ if (existingStyle) {
214
+ existingStyle.textContent = css;
215
+ return existingStyle;
216
+ }
217
+ const style = document.createElement("style");
218
+ style.id = styleId;
219
+ style.textContent = css;
220
+ document.head.appendChild(style);
221
+ return style;
222
+ }
223
+ function removeCustomStyles(id) {
224
+ if (typeof document === "undefined") return;
225
+ const style = document.getElementById(id);
226
+ if (style && style.parentNode) {
227
+ style.parentNode.removeChild(style);
228
+ }
229
+ }
230
+ var DEFAULT_SERVLY_TAILWIND_CONFIG = {
231
+ theme: {
232
+ extend: {
233
+ // Add any Servly-specific theme extensions here
234
+ }
235
+ },
236
+ // Safelist common dynamic classes
237
+ safelist: [
238
+ // Spacing
239
+ { pattern: /^(p|m|gap)-/ },
240
+ // Sizing
241
+ { pattern: /^(w|h|min-w|min-h|max-w|max-h)-/ },
242
+ // Flexbox
243
+ { pattern: /^(flex|justify|items|self)-/ },
244
+ // Grid
245
+ { pattern: /^(grid|col|row)-/ },
246
+ // Colors
247
+ { pattern: /^(bg|text|border|ring)-/ },
248
+ // Typography
249
+ { pattern: /^(font|text|leading|tracking)-/ },
250
+ // Borders
251
+ { pattern: /^(rounded|border)-/ },
252
+ // Effects
253
+ { pattern: /^(shadow|opacity|blur)-/ },
254
+ // Transforms
255
+ { pattern: /^(scale|rotate|translate|skew)-/ },
256
+ // Transitions
257
+ { pattern: /^(transition|duration|ease|delay)-/ }
258
+ ]
259
+ };
260
+ async function initServlyTailwind(customConfig) {
261
+ const config = customConfig ? { ...DEFAULT_SERVLY_TAILWIND_CONFIG, ...customConfig } : DEFAULT_SERVLY_TAILWIND_CONFIG;
262
+ await injectTailwind({
263
+ config,
264
+ usePlayCdn: true
265
+ });
266
+ }
267
+ var injectTailwindStyles = initServlyTailwind;
268
+ var tailwind_default = {
269
+ injectTailwind,
270
+ injectTailwindStyles,
271
+ removeTailwind,
272
+ isTailwindLoaded,
273
+ isTailwindReady,
274
+ waitForTailwind,
275
+ getTailwind,
276
+ updateTailwindConfig,
277
+ addCustomStyles,
278
+ removeCustomStyles,
279
+ initServlyTailwind,
280
+ preventFOUC,
281
+ removeFOUCPrevention,
282
+ markElementReady,
283
+ preloadTailwind,
284
+ DEFAULT_SERVLY_TAILWIND_CONFIG
285
+ };
286
+
287
+ export {
288
+ preloadTailwind,
289
+ preventFOUC,
290
+ removeFOUCPrevention,
291
+ markElementReady,
292
+ isTailwindReady,
293
+ waitForTailwind,
294
+ injectTailwind,
295
+ removeTailwind,
296
+ isTailwindLoaded,
297
+ getTailwind,
298
+ updateTailwindConfig,
299
+ addCustomStyles,
300
+ removeCustomStyles,
301
+ DEFAULT_SERVLY_TAILWIND_CONFIG,
302
+ initServlyTailwind,
303
+ injectTailwindStyles,
304
+ tailwind_default
305
+ };