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.
- package/CHANGELOG.md +466 -0
- package/README.md +198 -0
- package/package.json +91 -0
- package/src/components/base-component.js +925 -0
- package/src/core/component-manager.js +306 -0
- package/src/core/dom-morph.js +234 -0
- package/src/core/event-bus.js +229 -0
- package/src/core/router.js +487 -0
- package/src/core/signal.js +114 -0
- package/src/framework.js +323 -0
- package/src/plugins/alerts/alerts-plugin.js +427 -0
- package/src/plugins/fonts/files/inter.js +4 -0
- package/src/plugins/fonts/files/jetbrains-mono.js +4 -0
- package/src/plugins/fonts/font-manifests.js +53 -0
- package/src/plugins/fonts/fonts-plugin.js +246 -0
- package/src/plugins/icons/default-icons.js +51 -0
- package/src/plugins/icons/icons-plugin.js +130 -0
- package/src/plugins/store/store-plugin.js +127 -0
- package/src/plugins/theme/base-styles.js +58 -0
- package/src/plugins/theme/theme-plugin.js +160 -0
- package/src/utils/decorators.js +51 -0
- package/src/utils/dom.js +40 -0
- package/src/utils/error-handler.js +442 -0
- package/src/utils/framework-debug.js +375 -0
- package/src/utils/logger.js +324 -0
- package/src/utils/notification.js +123 -0
- package/src/utils/performance.js +281 -0
- package/src/utils/storage.js +86 -0
- package/src/utils/sweet-alert.js +84 -0
- package/src/utils/validation.js +70 -0
- package/src/utils/validators.js +129 -0
- package/types/index.d.ts +524 -0
|
@@ -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
|
+
}
|
package/src/framework.js
ADDED
|
@@ -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';
|