pulse-js-framework 1.4.7 → 1.4.9
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 +70 -5
- package/cli/dev.js +24 -1
- package/cli/lint.js +117 -0
- package/compiler/index.js +33 -3
- package/compiler/sourcemap.js +360 -0
- package/compiler/transformer.js +108 -2
- package/package.json +4 -2
- package/runtime/router.js +374 -8
- package/types/hmr.d.ts +112 -0
- package/types/index.d.ts +25 -1
- package/types/router.d.ts +89 -0
- package/types/sourcemap.d.ts +126 -0
package/runtime/router.js
CHANGED
|
@@ -10,11 +10,328 @@
|
|
|
10
10
|
* - Per-route and global guards
|
|
11
11
|
* - Scroll restoration
|
|
12
12
|
* - Lazy-loaded routes
|
|
13
|
+
* - Middleware support
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { pulse, effect, batch } from './pulse.js';
|
|
16
17
|
import { el } from './dom.js';
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Lazy load helper for route components
|
|
21
|
+
* Wraps a dynamic import to provide loading states and error handling
|
|
22
|
+
*
|
|
23
|
+
* @param {function} importFn - Dynamic import function () => import('./Component.js')
|
|
24
|
+
* @param {Object} options - Lazy loading options
|
|
25
|
+
* @param {function} options.loading - Loading component function
|
|
26
|
+
* @param {function} options.error - Error component function
|
|
27
|
+
* @param {number} options.timeout - Timeout in ms (default: 10000)
|
|
28
|
+
* @param {number} options.delay - Delay before showing loading (default: 200)
|
|
29
|
+
* @returns {function} Lazy route handler
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const routes = {
|
|
33
|
+
* '/dashboard': lazy(() => import('./Dashboard.js')),
|
|
34
|
+
* '/settings': lazy(() => import('./Settings.js'), {
|
|
35
|
+
* loading: () => el('div.spinner', 'Loading...'),
|
|
36
|
+
* error: (err) => el('div.error', `Failed to load: ${err.message}`),
|
|
37
|
+
* timeout: 5000
|
|
38
|
+
* })
|
|
39
|
+
* };
|
|
40
|
+
*/
|
|
41
|
+
export function lazy(importFn, options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
loading: LoadingComponent = null,
|
|
44
|
+
error: ErrorComponent = null,
|
|
45
|
+
timeout = 10000,
|
|
46
|
+
delay = 200
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
// Cache for loaded component
|
|
50
|
+
let cachedComponent = null;
|
|
51
|
+
let loadPromise = null;
|
|
52
|
+
|
|
53
|
+
return function lazyHandler(ctx) {
|
|
54
|
+
// Return cached component if already loaded
|
|
55
|
+
if (cachedComponent) {
|
|
56
|
+
return typeof cachedComponent === 'function'
|
|
57
|
+
? cachedComponent(ctx)
|
|
58
|
+
: cachedComponent.default
|
|
59
|
+
? cachedComponent.default(ctx)
|
|
60
|
+
: cachedComponent.render
|
|
61
|
+
? cachedComponent.render(ctx)
|
|
62
|
+
: cachedComponent;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create container for async loading
|
|
66
|
+
const container = el('div.lazy-route');
|
|
67
|
+
let loadingTimer = null;
|
|
68
|
+
let timeoutTimer = null;
|
|
69
|
+
|
|
70
|
+
// Start loading if not already
|
|
71
|
+
if (!loadPromise) {
|
|
72
|
+
loadPromise = importFn();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Delay showing loading state to avoid flash
|
|
76
|
+
if (LoadingComponent && delay > 0) {
|
|
77
|
+
loadingTimer = setTimeout(() => {
|
|
78
|
+
if (!cachedComponent) {
|
|
79
|
+
container.replaceChildren(LoadingComponent());
|
|
80
|
+
}
|
|
81
|
+
}, delay);
|
|
82
|
+
} else if (LoadingComponent) {
|
|
83
|
+
container.replaceChildren(LoadingComponent());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Set timeout for loading
|
|
87
|
+
const timeoutPromise = timeout > 0
|
|
88
|
+
? new Promise((_, reject) => {
|
|
89
|
+
timeoutTimer = setTimeout(() => {
|
|
90
|
+
reject(new Error(`Lazy load timeout after ${timeout}ms`));
|
|
91
|
+
}, timeout);
|
|
92
|
+
})
|
|
93
|
+
: null;
|
|
94
|
+
|
|
95
|
+
// Race between load and timeout
|
|
96
|
+
const loadWithTimeout = timeoutPromise
|
|
97
|
+
? Promise.race([loadPromise, timeoutPromise])
|
|
98
|
+
: loadPromise;
|
|
99
|
+
|
|
100
|
+
loadWithTimeout
|
|
101
|
+
.then(module => {
|
|
102
|
+
clearTimeout(loadingTimer);
|
|
103
|
+
clearTimeout(timeoutTimer);
|
|
104
|
+
|
|
105
|
+
// Cache the component
|
|
106
|
+
cachedComponent = module;
|
|
107
|
+
|
|
108
|
+
// Get the component from module
|
|
109
|
+
const Component = module.default || module;
|
|
110
|
+
const result = typeof Component === 'function'
|
|
111
|
+
? Component(ctx)
|
|
112
|
+
: Component.render
|
|
113
|
+
? Component.render(ctx)
|
|
114
|
+
: Component;
|
|
115
|
+
|
|
116
|
+
// Replace loading with actual component
|
|
117
|
+
if (result instanceof Node) {
|
|
118
|
+
container.replaceChildren(result);
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.catch(err => {
|
|
122
|
+
clearTimeout(loadingTimer);
|
|
123
|
+
clearTimeout(timeoutTimer);
|
|
124
|
+
loadPromise = null; // Allow retry
|
|
125
|
+
|
|
126
|
+
if (ErrorComponent) {
|
|
127
|
+
container.replaceChildren(ErrorComponent(err));
|
|
128
|
+
} else {
|
|
129
|
+
console.error('Lazy load error:', err);
|
|
130
|
+
container.replaceChildren(
|
|
131
|
+
el('div.lazy-error', `Failed to load component: ${err.message}`)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return container;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Preload a lazy component without rendering
|
|
142
|
+
* Useful for prefetching on hover or when likely to navigate
|
|
143
|
+
*
|
|
144
|
+
* @param {function} lazyHandler - Lazy handler created with lazy()
|
|
145
|
+
* @returns {Promise} Resolves when component is loaded
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const DashboardLazy = lazy(() => import('./Dashboard.js'));
|
|
149
|
+
* // Preload on link hover
|
|
150
|
+
* link.addEventListener('mouseenter', () => preload(DashboardLazy));
|
|
151
|
+
*/
|
|
152
|
+
export function preload(lazyHandler) {
|
|
153
|
+
// Trigger the lazy handler with a dummy context to start loading
|
|
154
|
+
// The result is discarded, but the component will be cached
|
|
155
|
+
return new Promise(resolve => {
|
|
156
|
+
const result = lazyHandler({});
|
|
157
|
+
if (result instanceof Promise) {
|
|
158
|
+
result.then(resolve);
|
|
159
|
+
} else {
|
|
160
|
+
// Already loaded
|
|
161
|
+
resolve(result);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Middleware context passed to each middleware function
|
|
168
|
+
* @typedef {Object} MiddlewareContext
|
|
169
|
+
* @property {NavigationTarget} to - Target route
|
|
170
|
+
* @property {NavigationTarget} from - Source route
|
|
171
|
+
* @property {Object} meta - Shared metadata between middlewares
|
|
172
|
+
* @property {function} redirect - Redirect to another path
|
|
173
|
+
* @property {function} abort - Abort navigation
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a middleware runner for the router
|
|
178
|
+
* Middlewares are executed in order, each can modify context or abort navigation
|
|
179
|
+
*
|
|
180
|
+
* @param {Array<function>} middlewares - Array of middleware functions
|
|
181
|
+
* @returns {function} Runner function
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* const authMiddleware = async (ctx, next) => {
|
|
185
|
+
* if (ctx.to.meta.requiresAuth && !isAuthenticated()) {
|
|
186
|
+
* return ctx.redirect('/login');
|
|
187
|
+
* }
|
|
188
|
+
* await next();
|
|
189
|
+
* };
|
|
190
|
+
*
|
|
191
|
+
* const loggerMiddleware = async (ctx, next) => {
|
|
192
|
+
* console.log('Navigating to:', ctx.to.path);
|
|
193
|
+
* const start = Date.now();
|
|
194
|
+
* await next();
|
|
195
|
+
* console.log('Navigation took:', Date.now() - start, 'ms');
|
|
196
|
+
* };
|
|
197
|
+
*
|
|
198
|
+
* const router = createRouter({
|
|
199
|
+
* routes,
|
|
200
|
+
* middleware: [loggerMiddleware, authMiddleware]
|
|
201
|
+
* });
|
|
202
|
+
*/
|
|
203
|
+
function createMiddlewareRunner(middlewares) {
|
|
204
|
+
return async function runMiddleware(context) {
|
|
205
|
+
let index = 0;
|
|
206
|
+
let aborted = false;
|
|
207
|
+
let redirectPath = null;
|
|
208
|
+
|
|
209
|
+
// Create enhanced context with redirect and abort
|
|
210
|
+
const ctx = {
|
|
211
|
+
...context,
|
|
212
|
+
meta: {},
|
|
213
|
+
redirect: (path) => {
|
|
214
|
+
redirectPath = path;
|
|
215
|
+
},
|
|
216
|
+
abort: () => {
|
|
217
|
+
aborted = true;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
async function next() {
|
|
222
|
+
if (aborted || redirectPath) return;
|
|
223
|
+
if (index >= middlewares.length) return;
|
|
224
|
+
|
|
225
|
+
const middleware = middlewares[index++];
|
|
226
|
+
await middleware(ctx, next);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await next();
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
aborted,
|
|
233
|
+
redirectPath,
|
|
234
|
+
meta: ctx.meta
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Radix Trie for efficient route matching
|
|
241
|
+
* Provides O(path length) lookup instead of O(routes count)
|
|
242
|
+
*/
|
|
243
|
+
class RouteTrie {
|
|
244
|
+
constructor() {
|
|
245
|
+
this.root = { children: new Map(), route: null, paramName: null, isWildcard: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Insert a route into the trie
|
|
250
|
+
*/
|
|
251
|
+
insert(pattern, route) {
|
|
252
|
+
const segments = pattern === '/' ? [''] : pattern.split('/').filter(Boolean);
|
|
253
|
+
let node = this.root;
|
|
254
|
+
|
|
255
|
+
for (const segment of segments) {
|
|
256
|
+
let key;
|
|
257
|
+
let paramName = null;
|
|
258
|
+
let isWildcard = false;
|
|
259
|
+
|
|
260
|
+
if (segment.startsWith(':')) {
|
|
261
|
+
// Dynamic segment - :param
|
|
262
|
+
key = ':';
|
|
263
|
+
paramName = segment.slice(1);
|
|
264
|
+
} else if (segment.startsWith('*')) {
|
|
265
|
+
// Wildcard segment - *path
|
|
266
|
+
key = '*';
|
|
267
|
+
paramName = segment.slice(1) || 'wildcard';
|
|
268
|
+
isWildcard = true;
|
|
269
|
+
} else {
|
|
270
|
+
// Static segment
|
|
271
|
+
key = segment;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!node.children.has(key)) {
|
|
275
|
+
node.children.set(key, {
|
|
276
|
+
children: new Map(),
|
|
277
|
+
route: null,
|
|
278
|
+
paramName,
|
|
279
|
+
isWildcard
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
node = node.children.get(key);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
node.route = route;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Find a matching route for a path
|
|
290
|
+
*/
|
|
291
|
+
find(path) {
|
|
292
|
+
const segments = path === '/' ? [''] : path.split('/').filter(Boolean);
|
|
293
|
+
return this._findRecursive(this.root, segments, 0, {});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_findRecursive(node, segments, index, params) {
|
|
297
|
+
// End of path
|
|
298
|
+
if (index === segments.length) {
|
|
299
|
+
if (node.route) {
|
|
300
|
+
return { route: node.route, params };
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const segment = segments[index];
|
|
306
|
+
|
|
307
|
+
// Try static match first (most specific)
|
|
308
|
+
if (node.children.has(segment)) {
|
|
309
|
+
const result = this._findRecursive(node.children.get(segment), segments, index + 1, params);
|
|
310
|
+
if (result) return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Try dynamic param match
|
|
314
|
+
if (node.children.has(':')) {
|
|
315
|
+
const paramNode = node.children.get(':');
|
|
316
|
+
const newParams = { ...params, [paramNode.paramName]: decodeURIComponent(segment) };
|
|
317
|
+
const result = this._findRecursive(paramNode, segments, index + 1, newParams);
|
|
318
|
+
if (result) return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Try wildcard match (catches all remaining segments)
|
|
322
|
+
if (node.children.has('*')) {
|
|
323
|
+
const wildcardNode = node.children.get('*');
|
|
324
|
+
const remaining = segments.slice(index).map(decodeURIComponent).join('/');
|
|
325
|
+
return {
|
|
326
|
+
route: wildcardNode.route,
|
|
327
|
+
params: { ...params, [wildcardNode.paramName]: remaining }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
18
335
|
/**
|
|
19
336
|
* Parse a route pattern into a regex and extract param names
|
|
20
337
|
* Supports: /users/:id, /posts/:id/comments, /files/*path, * (catch-all)
|
|
@@ -126,9 +443,13 @@ export function createRouter(options = {}) {
|
|
|
126
443
|
routes = {},
|
|
127
444
|
mode = 'history', // 'history' or 'hash'
|
|
128
445
|
base = '',
|
|
129
|
-
scrollBehavior = null // Function to control scroll restoration
|
|
446
|
+
scrollBehavior = null, // Function to control scroll restoration
|
|
447
|
+
middleware: initialMiddleware = [] // Middleware functions
|
|
130
448
|
} = options;
|
|
131
449
|
|
|
450
|
+
// Middleware array (mutable for dynamic registration)
|
|
451
|
+
const middleware = [...initialMiddleware];
|
|
452
|
+
|
|
132
453
|
// Reactive state
|
|
133
454
|
const currentPath = pulse(getPath());
|
|
134
455
|
const currentRoute = pulse(null);
|
|
@@ -140,6 +461,9 @@ export function createRouter(options = {}) {
|
|
|
140
461
|
// Scroll positions for history
|
|
141
462
|
const scrollPositions = new Map();
|
|
142
463
|
|
|
464
|
+
// Route trie for O(path length) lookups
|
|
465
|
+
const routeTrie = new RouteTrie();
|
|
466
|
+
|
|
143
467
|
// Compile routes (supports nested routes)
|
|
144
468
|
const compiledRoutes = [];
|
|
145
469
|
|
|
@@ -148,11 +472,16 @@ export function createRouter(options = {}) {
|
|
|
148
472
|
const normalized = normalizeRoute(pattern, config);
|
|
149
473
|
const fullPattern = parentPath + pattern;
|
|
150
474
|
|
|
151
|
-
|
|
475
|
+
const route = {
|
|
152
476
|
...normalized,
|
|
153
477
|
pattern: fullPattern,
|
|
154
478
|
...parsePattern(fullPattern)
|
|
155
|
-
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
compiledRoutes.push(route);
|
|
482
|
+
|
|
483
|
+
// Insert into trie for fast lookup
|
|
484
|
+
routeTrie.insert(fullPattern, route);
|
|
156
485
|
|
|
157
486
|
// Compile children (nested routes)
|
|
158
487
|
if (normalized.children) {
|
|
@@ -183,15 +512,22 @@ export function createRouter(options = {}) {
|
|
|
183
512
|
}
|
|
184
513
|
|
|
185
514
|
/**
|
|
186
|
-
* Find matching route
|
|
515
|
+
* Find matching route using trie for O(path length) lookup
|
|
187
516
|
*/
|
|
188
517
|
function findRoute(path) {
|
|
518
|
+
// Use trie for efficient lookup
|
|
519
|
+
const result = routeTrie.find(path);
|
|
520
|
+
if (result) {
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Fallback to catch-all route if exists
|
|
189
525
|
for (const route of compiledRoutes) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return { route, params };
|
|
526
|
+
if (route.pattern === '*') {
|
|
527
|
+
return { route, params: {} };
|
|
193
528
|
}
|
|
194
529
|
}
|
|
530
|
+
|
|
195
531
|
return null;
|
|
196
532
|
}
|
|
197
533
|
|
|
@@ -234,6 +570,20 @@ export function createRouter(options = {}) {
|
|
|
234
570
|
meta: match?.route?.meta || {}
|
|
235
571
|
};
|
|
236
572
|
|
|
573
|
+
// Run middleware if configured
|
|
574
|
+
if (middleware.length > 0) {
|
|
575
|
+
const runMiddleware = createMiddlewareRunner(middleware);
|
|
576
|
+
const middlewareResult = await runMiddleware({ to, from });
|
|
577
|
+
if (middlewareResult.aborted) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (middlewareResult.redirectPath) {
|
|
581
|
+
return navigate(middlewareResult.redirectPath, { replace: true });
|
|
582
|
+
}
|
|
583
|
+
// Merge middleware meta into route meta
|
|
584
|
+
Object.assign(to.meta, middlewareResult.meta);
|
|
585
|
+
}
|
|
586
|
+
|
|
237
587
|
// Run global beforeEach hooks
|
|
238
588
|
for (const hook of beforeHooks) {
|
|
239
589
|
const result = await hook(to, from);
|
|
@@ -427,7 +777,7 @@ export function createRouter(options = {}) {
|
|
|
427
777
|
// Cleanup previous view
|
|
428
778
|
if (cleanup) cleanup();
|
|
429
779
|
if (currentView) {
|
|
430
|
-
container.
|
|
780
|
+
container.replaceChildren();
|
|
431
781
|
}
|
|
432
782
|
|
|
433
783
|
if (route && route.handler) {
|
|
@@ -466,6 +816,19 @@ export function createRouter(options = {}) {
|
|
|
466
816
|
return container;
|
|
467
817
|
}
|
|
468
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Add middleware dynamically
|
|
821
|
+
* @param {function} middlewareFn - Middleware function (ctx, next) => {}
|
|
822
|
+
* @returns {function} Unregister function
|
|
823
|
+
*/
|
|
824
|
+
function use(middlewareFn) {
|
|
825
|
+
middleware.push(middlewareFn);
|
|
826
|
+
return () => {
|
|
827
|
+
const index = middleware.indexOf(middlewareFn);
|
|
828
|
+
if (index > -1) middleware.splice(index, 1);
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
469
832
|
/**
|
|
470
833
|
* Add navigation guard
|
|
471
834
|
*/
|
|
@@ -559,6 +922,7 @@ export function createRouter(options = {}) {
|
|
|
559
922
|
start,
|
|
560
923
|
link,
|
|
561
924
|
outlet,
|
|
925
|
+
use,
|
|
562
926
|
beforeEach,
|
|
563
927
|
beforeResolve,
|
|
564
928
|
afterEach,
|
|
@@ -591,6 +955,8 @@ export function simpleRouter(routes, target = '#app') {
|
|
|
591
955
|
export default {
|
|
592
956
|
createRouter,
|
|
593
957
|
simpleRouter,
|
|
958
|
+
lazy,
|
|
959
|
+
preload,
|
|
594
960
|
matchRoute,
|
|
595
961
|
parseQuery
|
|
596
962
|
};
|
package/types/hmr.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Framework - HMR (Hot Module Replacement) Type Definitions
|
|
3
|
+
* @module pulse-js-framework/runtime/hmr
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Pulse, PulseOptions } from './pulse';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* HMR Context interface for state preservation and effect cleanup
|
|
10
|
+
*/
|
|
11
|
+
export interface HMRContext {
|
|
12
|
+
/**
|
|
13
|
+
* Persistent data storage across HMR updates.
|
|
14
|
+
* Values stored here survive module reloads.
|
|
15
|
+
*/
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a pulse with state preservation across HMR updates.
|
|
20
|
+
* If a value exists from a previous module load, it's restored.
|
|
21
|
+
*
|
|
22
|
+
* @param key - Unique key for this pulse within the module
|
|
23
|
+
* @param initialValue - Initial value (used on first load only)
|
|
24
|
+
* @param options - Pulse options (equals function, etc.)
|
|
25
|
+
* @returns A pulse instance with preserved state
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const count = hmr.preservePulse('count', 0);
|
|
29
|
+
* const todos = hmr.preservePulse('todos', [], { equals: deepEquals });
|
|
30
|
+
*/
|
|
31
|
+
preservePulse<T>(key: string, initialValue: T, options?: PulseOptions<T>): Pulse<T>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute code with module tracking enabled.
|
|
35
|
+
* Effects created within this callback will be registered
|
|
36
|
+
* for automatic cleanup during HMR.
|
|
37
|
+
*
|
|
38
|
+
* @param callback - Code to execute with tracking
|
|
39
|
+
* @returns The return value of the callback
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* hmr.setup(() => {
|
|
43
|
+
* effect(() => {
|
|
44
|
+
* document.title = `Count: ${count.get()}`;
|
|
45
|
+
* });
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
setup<T>(callback: () => T): T;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a callback to run when the module accepts an HMR update.
|
|
52
|
+
* Call without arguments to auto-accept updates.
|
|
53
|
+
*
|
|
54
|
+
* @param callback - Optional callback for custom handling
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* hmr.accept(); // Auto-accept
|
|
58
|
+
* hmr.accept(() => console.log('Module updated!'));
|
|
59
|
+
*/
|
|
60
|
+
accept(callback?: () => void): void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a callback to run before the module is replaced.
|
|
64
|
+
* Use this for custom cleanup logic.
|
|
65
|
+
*
|
|
66
|
+
* @param callback - Cleanup callback
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* hmr.dispose(() => {
|
|
70
|
+
* socket.close();
|
|
71
|
+
* clearInterval(timer);
|
|
72
|
+
* });
|
|
73
|
+
*/
|
|
74
|
+
dispose(callback: () => void): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create an HMR context for a module.
|
|
79
|
+
* Provides utilities for state preservation and effect cleanup during HMR.
|
|
80
|
+
*
|
|
81
|
+
* In production or non-HMR environments, returns a no-op context
|
|
82
|
+
* that works normally but without HMR-specific behavior.
|
|
83
|
+
*
|
|
84
|
+
* @param moduleId - The module identifier (typically import.meta.url)
|
|
85
|
+
* @returns HMR context with preservation utilities
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* import { createHMRContext } from 'pulse-js-framework/runtime/hmr';
|
|
89
|
+
*
|
|
90
|
+
* const hmr = createHMRContext(import.meta.url);
|
|
91
|
+
*
|
|
92
|
+
* // Preserve state across HMR
|
|
93
|
+
* const todos = hmr.preservePulse('todos', []);
|
|
94
|
+
* const filter = hmr.preservePulse('filter', 'all');
|
|
95
|
+
*
|
|
96
|
+
* // Setup effects with automatic cleanup
|
|
97
|
+
* hmr.setup(() => {
|
|
98
|
+
* effect(() => {
|
|
99
|
+
* document.title = `${todos.get().length} todos`;
|
|
100
|
+
* });
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // Accept HMR updates
|
|
104
|
+
* hmr.accept();
|
|
105
|
+
*/
|
|
106
|
+
export function createHMRContext(moduleId: string): HMRContext;
|
|
107
|
+
|
|
108
|
+
declare const hmr: {
|
|
109
|
+
createHMRContext: typeof createHMRContext;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default hmr;
|
package/types/index.d.ts
CHANGED
|
@@ -90,8 +90,15 @@ export {
|
|
|
90
90
|
LinkOptions,
|
|
91
91
|
MatchedRoute,
|
|
92
92
|
Router,
|
|
93
|
+
MiddlewareContext,
|
|
94
|
+
MiddlewareFn,
|
|
95
|
+
LazyOptions,
|
|
96
|
+
LazyRouteHandler,
|
|
97
|
+
RouteContext,
|
|
93
98
|
createRouter,
|
|
94
|
-
simpleRouter
|
|
99
|
+
simpleRouter,
|
|
100
|
+
lazy,
|
|
101
|
+
preload
|
|
95
102
|
} from './router';
|
|
96
103
|
|
|
97
104
|
// Store
|
|
@@ -137,3 +144,20 @@ export {
|
|
|
137
144
|
logger,
|
|
138
145
|
loggers
|
|
139
146
|
} from './logger';
|
|
147
|
+
|
|
148
|
+
// HMR (Hot Module Replacement)
|
|
149
|
+
export {
|
|
150
|
+
HMRContext,
|
|
151
|
+
createHMRContext
|
|
152
|
+
} from './hmr';
|
|
153
|
+
|
|
154
|
+
// Source Maps (Compiler)
|
|
155
|
+
export {
|
|
156
|
+
Position,
|
|
157
|
+
Mapping,
|
|
158
|
+
SourceMapV3,
|
|
159
|
+
OriginalPosition,
|
|
160
|
+
SourceMapGenerator,
|
|
161
|
+
SourceMapConsumer,
|
|
162
|
+
encodeVLQ
|
|
163
|
+
} from './sourcemap';
|