vanillaforge 1.9.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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Signal — a reactive primitive for VanillaForge.
3
+ *
4
+ * A Signal holds a single value. Reading `.value` returns the current value.
5
+ * Calling `.set(newValue)` updates the value and:
6
+ * - notifies all `.subscribe()` listeners immediately, and
7
+ * - schedules a single morph-based re-render of the linked component in the
8
+ * next microtask (multiple `.set()` calls in the same synchronous block
9
+ * are automatically batched into one render).
10
+ *
11
+ * Usage inside a component:
12
+ *
13
+ * constructor(eventBus, props) {
14
+ * super(eventBus, props);
15
+ * this.count = this.signal(0); // linked to this component
16
+ * }
17
+ *
18
+ * getTemplate() {
19
+ * return `<p>${this.count.value}</p>`;
20
+ * }
21
+ *
22
+ * getMethods() {
23
+ * return {
24
+ * inc: () => this.count.set(this.count.value + 1),
25
+ * };
26
+ * }
27
+ *
28
+ * Standalone usage (no component):
29
+ *
30
+ * import { Signal } from 'vanillaforge';
31
+ * const name = new Signal('world');
32
+ * const unsub = name.subscribe((v) => console.log('Hello', v));
33
+ * name.set('VanillaForge'); // logs: Hello VanillaForge
34
+ * unsub(); // stop listening
35
+ *
36
+ * Note on fine-grained DOM patching:
37
+ * The current implementation triggers a full morph re-render on change —
38
+ * the same efficient morph used by setState(), but batched across multiple
39
+ * signal updates. True text-node-level patching (bypassing getTemplate()
40
+ * entirely) requires a tagged-template helper and is planned for a future
41
+ * release.
42
+ */
43
+
44
+ export class Signal {
45
+ /**
46
+ * @param {*} initialValue
47
+ */
48
+ constructor(initialValue) {
49
+ this._value = initialValue;
50
+ this._subscribers = new Set();
51
+ this._component = null;
52
+ this._destroyed = false;
53
+ }
54
+
55
+ /** The current value. */
56
+ get value() {
57
+ return this._value;
58
+ }
59
+
60
+ /**
61
+ * Update the value. Identical values (via Object.is) are ignored.
62
+ * Notifies subscribers and schedules a component re-render.
63
+ * @param {*} newValue
64
+ */
65
+ set(newValue) {
66
+ if (this._destroyed || Object.is(this._value, newValue)) return;
67
+ this._value = newValue;
68
+ for (const fn of this._subscribers) {
69
+ try {
70
+ fn(newValue);
71
+ } catch (err) {
72
+ // Subscriber errors must not prevent other subscribers from running.
73
+ console.error('[Signal] subscriber error', err);
74
+ }
75
+ }
76
+ if (this._component) {
77
+ this._component._scheduleSignalRender();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Subscribe to value changes. The handler is called with the new value each
83
+ * time `.set()` is called with a different value.
84
+ *
85
+ * Returns an unsubscribe function.
86
+ *
87
+ * @param {(value: *) => void} fn
88
+ * @returns {() => void}
89
+ */
90
+ subscribe(fn) {
91
+ this._subscribers.add(fn);
92
+ return () => this._subscribers.delete(fn);
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Internal API — used by BaseComponent, not part of the public surface
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Link this signal to a component so .set() triggers its render. */
100
+ _link(component) {
101
+ this._component = component;
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * Detach from everything. Called when the owning component is destroyed.
107
+ * After this, .set() is a safe no-op.
108
+ */
109
+ _destroy() {
110
+ this._destroyed = true;
111
+ this._subscribers.clear();
112
+ this._component = null;
113
+ }
114
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * VanillaForge Framework
3
+ *
4
+ * A modern, lightweight framework for forging Single Page Applications
5
+ * with vanilla JavaScript. No external dependencies required.
6
+ *
7
+ * Features:
8
+ * - Component-based architecture
9
+ * - Client-side routing
10
+ * - Event-driven communication
11
+ * - State management
12
+ * - Error handling and logging
13
+ * - Build system
14
+ *
15
+ * @author Stephen Musyoka - VanillaForge Creator
16
+ * @version 1.9.0
17
+ * @since 2025-06-15
18
+ */
19
+
20
+ // Core Framework Components
21
+ export { ComponentManager } from './core/component-manager.js';
22
+ export { Router } from './core/router.js';
23
+ export { EventBus } from './core/event-bus.js';
24
+
25
+ // Base Classes
26
+ export { BaseComponent } from './components/base-component.js';
27
+
28
+ // Utilities
29
+ export { Logger } from './utils/logger.js';
30
+ export { ErrorHandler, ErrorType } from './utils/error-handler.js';
31
+ export { FrameworkDebug } from './utils/framework-debug.js';
32
+ export { SweetAlert } from './utils/sweet-alert.js';
33
+ export { ValidationUtils } from './utils/validation.js';
34
+ export { PerformanceUtils, performanceUtils } from './utils/performance.js';
35
+ export { perf, cache } from './utils/decorators.js';
36
+ export { optimizeImage, batchDOMOperations } from './utils/dom.js';
37
+
38
+ // Core reactive primitive
39
+ export { Signal } from './core/signal.js';
40
+
41
+ // First-party plugins
42
+ export { iconsPlugin, IconsService } from './plugins/icons/icons-plugin.js';
43
+ export { themePlugin, ThemeService } from './plugins/theme/theme-plugin.js';
44
+ export { alertsPlugin, AlertsService } from './plugins/alerts/alerts-plugin.js';
45
+ export { fontsPlugin, FontsService } from './plugins/fonts/fonts-plugin.js';
46
+ export { storePlugin, StoreService } from './plugins/store/store-plugin.js';
47
+
48
+ // Import classes for internal use
49
+ import { ComponentManager } from './core/component-manager.js';
50
+ import { Router } from './core/router.js';
51
+ import { EventBus } from './core/event-bus.js';
52
+ import { Logger } from './utils/logger.js';
53
+ import { ErrorHandler } from './utils/error-handler.js';
54
+ import { Notification } from './utils/notification.js';
55
+ import { LocalStorageAdapter } from './utils/storage.js';
56
+ import { ValidationUtils } from './utils/validation.js';
57
+ import { FrameworkDebug } from './utils/framework-debug.js';
58
+ import { performanceUtils } from './utils/performance.js';
59
+
60
+ /**
61
+ * Framework Application Class
62
+ *
63
+ * Main application class that initializes and manages VanillaForge
64
+ */
65
+ export class FrameworkApp {
66
+ constructor(config = {}) {
67
+ this.config = {
68
+ appName: 'VanillaForge App',
69
+ debug: false,
70
+ // DOM id that route components are mounted into.
71
+ mountId: 'main-content',
72
+ router: {
73
+ mode: 'history',
74
+ fallback: '/404'
75
+ },
76
+ logging: {
77
+ level: 'info',
78
+ console: true
79
+ },
80
+ ...config
81
+ };
82
+
83
+ // Service registry: all framework and plugin services live here.
84
+ // Keys are string names; values are service instances.
85
+ this._services = new Map();
86
+ // Track installed plugin names to prevent double-installation.
87
+ this._installedPlugins = new Set();
88
+
89
+ const storageAdapter = new LocalStorageAdapter();
90
+ this.logger = new Logger('FrameworkApp', this.config.logging.level, storageAdapter);
91
+ this.eventBus = new EventBus(this.logger.child('EventBus'));
92
+ this.notification = new Notification();
93
+ this.errorHandler = new ErrorHandler(this.notification);
94
+ this.validation = new ValidationUtils(this.logger.child('Validation'));
95
+ this.componentManager = new ComponentManager(this.eventBus, this.logger.child('ComponentManager'), this.errorHandler, { mountId: this.config.mountId });
96
+ // Give the component manager a reference back to the app so it can
97
+ // wire instance.app on each mounted component.
98
+ this.componentManager.app = this;
99
+ this.router = null;
100
+ this.isInitialized = false;
101
+ this.performanceUtils = performanceUtils;
102
+
103
+ // Register built-in services so plugins and components can find them.
104
+ this._services.set('eventBus', this.eventBus);
105
+ this._services.set('logger', this.logger);
106
+ this._services.set('errorHandler', this.errorHandler);
107
+ this._services.set('notification', this.notification);
108
+ this._services.set('validation', this.validation);
109
+ this._services.set('componentManager', this.componentManager);
110
+ this._services.set('performanceUtils', this.performanceUtils);
111
+
112
+ // Enable debug mode if configured
113
+ if (this.config.debug) {
114
+ this.frameworkDebug = new FrameworkDebug(this);
115
+ this.frameworkDebug.enable();
116
+ }
117
+
118
+ this.logger.info('VanillaForge application created', this.config);
119
+ }
120
+
121
+ /**
122
+ * Register (or replace) a named service in the registry.
123
+ * Call this from a plugin's install() or from app-level setup code.
124
+ *
125
+ * @param {string} name - Service name, e.g. 'icons', 'theme', 'alerts'
126
+ * @param {*} instance - The service instance
127
+ * @returns {FrameworkApp} this, for chaining
128
+ */
129
+ provide(name, instance) {
130
+ this._services.set(name, instance);
131
+ this.logger.debug(`Service provided: ${name}`);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * Install a plugin. A plugin is either:
137
+ * - a function: (app, options) => void
138
+ * - an object: { name: string, install(app, options): void }
139
+ *
140
+ * The same plugin (identified by name or reference) is never installed twice.
141
+ * Plugins may call app.provide(), app.componentManager.registerComponent(), or
142
+ * subscribe to events. Plugins installed before app.initialize() run first.
143
+ *
144
+ * @param {Function|Object} plugin - Plugin function or object
145
+ * @param {Object} options - Options passed to the plugin's install function
146
+ * @returns {FrameworkApp} this, for chaining
147
+ */
148
+ use(plugin, options = {}) {
149
+ const pluginName = typeof plugin === 'object' ? plugin.name : plugin;
150
+ const pluginKey = pluginName || plugin;
151
+
152
+ if (this._installedPlugins.has(pluginKey)) {
153
+ this.logger.warn(`Plugin already installed, skipping: ${pluginName || '(anonymous)'}`);
154
+ return this;
155
+ }
156
+
157
+ if (typeof plugin === 'function') {
158
+ plugin(this, options);
159
+ } else if (plugin && typeof plugin.install === 'function') {
160
+ plugin.install(this, options);
161
+ } else {
162
+ throw new Error('VanillaForge plugin must be a function or an object with an install() method');
163
+ }
164
+
165
+ this._installedPlugins.add(pluginKey);
166
+ this.logger.debug(`Plugin installed: ${pluginName || '(anonymous)'}`);
167
+ return this;
168
+ }
169
+
170
+ /**
171
+ * Initialize the framework application
172
+ * @param {Object} options - Initialization options
173
+ */
174
+ async initialize(options = {}) {
175
+ if (this.isInitialized) {
176
+ this.logger.warn('Application already initialized');
177
+ return;
178
+ }
179
+ try {
180
+ this.logger.info('Initializing framework application...');
181
+
182
+ // Initialize component manager first (to set up event listeners)
183
+ await this.componentManager.initialize();
184
+
185
+ // Register components
186
+ if (options.components) {
187
+ Object.entries(options.components).forEach(([name, component]) => {
188
+ this.componentManager.registerComponent(name, component);
189
+ });
190
+ this.logger.info('Components registered', Object.keys(options.components));
191
+ }
192
+ // Initialize router if routing is enabled (after ComponentManager is ready)
193
+ if (options.routes) {
194
+ this.router = new Router(this.eventBus, this.logger.child('Router'), this.errorHandler, this.config.router);
195
+ // Make the router discoverable via app.get('router')
196
+ this._services.set('router', this.router);
197
+
198
+ // Register routes
199
+ Object.entries(options.routes).forEach(([path, component]) => {
200
+ this.router.addRoute(path, component);
201
+ });
202
+
203
+ await this.router.initialize();
204
+ this.logger.info('Router initialized with routes', Object.keys(options.routes));
205
+ }
206
+
207
+ this.isInitialized = true;
208
+ this.eventBus.emit('framework:initialized', { app: this });
209
+ this.logger.info('Framework application initialized successfully');
210
+
211
+ } catch (error) {
212
+ this.logger.error('Failed to initialize framework application', error);
213
+ this.errorHandler.handleError(error, 'APP_INIT_ERROR');
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Start the application
220
+ */
221
+ async start() {
222
+ if (!this.isInitialized) {
223
+ throw new Error('Application must be initialized before starting');
224
+ }
225
+
226
+ try {
227
+ this.logger.info('Starting framework application...');
228
+
229
+ // Start router if available
230
+ if (this.router) {
231
+ await this.router.start();
232
+ }
233
+
234
+ this.eventBus.emit('framework:started', { app: this });
235
+ this.logger.info('Framework application started successfully');
236
+
237
+ } catch (error) {
238
+ this.logger.error('Failed to start framework application', error);
239
+ this.errorHandler.handleError(error, 'APP_START_ERROR');
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Navigate to a route
246
+ * @param {string} path - Route path
247
+ * @param {Object} options - Navigation options
248
+ */
249
+ navigate(path, options = {}) {
250
+ if (this.router) {
251
+ return this.router.navigate(path, options);
252
+ } else {
253
+ this.logger.warn('Router not initialized, cannot navigate');
254
+ }
255
+ }
256
+ /**
257
+ * Retrieve a service by name.
258
+ * Checks the service registry first, then falls back to registered
259
+ * component classes (for backward-compatibility with existing app.get() calls).
260
+ *
261
+ * @param {string} name - Service name (e.g. 'router', 'icons', 'eventBus')
262
+ * @returns {*} Service instance, or null if not found
263
+ */
264
+ get(name) {
265
+ // Named service in the registry (covers both built-ins and plugins).
266
+ if (this._services.has(name)) {
267
+ return this._services.get(name);
268
+ }
269
+ // Router is registered lazily after initialize().
270
+ if (name === 'router') {
271
+ return this.router;
272
+ }
273
+ // Fall back to looking up a registered component class.
274
+ if (this.componentManager.components.has(name)) {
275
+ return this.componentManager.components.get(name);
276
+ }
277
+ return null;
278
+ }
279
+
280
+ /**
281
+ * Shutdown the application
282
+ */
283
+ async shutdown() {
284
+ try {
285
+ this.logger.info('Shutting down framework application...');
286
+
287
+ this.eventBus.emit('framework:shutdown', { app: this });
288
+
289
+ if (this.router) {
290
+ await this.router.cleanup();
291
+ }
292
+ await this.componentManager.cleanup();
293
+ this.eventBus.cleanup();
294
+
295
+ if (this.frameworkDebug) {
296
+ this.frameworkDebug.disable();
297
+ }
298
+
299
+ // Cleanup performance utilities
300
+ this.performanceUtils.cleanup();
301
+
302
+ this.isInitialized = false;
303
+ this.logger.info('Framework application shutdown complete');
304
+
305
+ } catch (error) {
306
+ this.logger.error('Error during application shutdown', error);
307
+ throw error;
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Create a new framework application instance
314
+ * @param {Object} config - Application configuration
315
+ * @returns {FrameworkApp} Framework application instance
316
+ */
317
+ export function createApp(config = {}) {
318
+ return new FrameworkApp(config);
319
+ }
320
+
321
+ // Framework metadata
322
+ export const FRAMEWORK_VERSION = '1.9.0';
323
+ export const FRAMEWORK_NAME = 'VanillaForge';