pulse-js-framework 1.4.8 → 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 +48 -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/compiler/transformer.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* - Import statement support
|
|
8
8
|
* - Slot-based component composition
|
|
9
9
|
* - CSS scoping with unique class prefixes
|
|
10
|
+
* - Source map generation
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { NodeType } from './parser.js';
|
|
14
|
+
import { SourceMapGenerator } from './sourcemap.js';
|
|
13
15
|
|
|
14
16
|
/** Generate a unique scope ID for CSS scoping */
|
|
15
17
|
const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 8);
|
|
@@ -29,13 +31,92 @@ const STATEMENT_TOKEN_TYPES = new Set(['IF', 'FOR', 'EACH']);
|
|
|
29
31
|
export class Transformer {
|
|
30
32
|
constructor(ast, options = {}) {
|
|
31
33
|
this.ast = ast;
|
|
32
|
-
this.options = {
|
|
34
|
+
this.options = {
|
|
35
|
+
runtime: 'pulse-js-framework/runtime',
|
|
36
|
+
minify: false,
|
|
37
|
+
scopeStyles: true,
|
|
38
|
+
sourceMap: false, // Enable source map generation
|
|
39
|
+
sourceFileName: null, // Original .pulse file name
|
|
40
|
+
sourceContent: null, // Original source content (for inline source maps)
|
|
41
|
+
...options
|
|
42
|
+
};
|
|
33
43
|
this.stateVars = new Set();
|
|
34
44
|
this.propVars = new Set();
|
|
35
45
|
this.propDefaults = new Map();
|
|
36
46
|
this.actionNames = new Set();
|
|
37
47
|
this.importedComponents = new Map();
|
|
38
48
|
this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
|
|
49
|
+
|
|
50
|
+
// Source map tracking
|
|
51
|
+
this.sourceMap = null;
|
|
52
|
+
this._currentLine = 0;
|
|
53
|
+
this._currentColumn = 0;
|
|
54
|
+
|
|
55
|
+
// Initialize source map generator if enabled
|
|
56
|
+
if (this.options.sourceMap) {
|
|
57
|
+
this.sourceMap = new SourceMapGenerator({
|
|
58
|
+
file: this.options.sourceFileName?.replace('.pulse', '.js') || 'output.js'
|
|
59
|
+
});
|
|
60
|
+
if (this.options.sourceFileName) {
|
|
61
|
+
this.sourceMap.addSource(
|
|
62
|
+
this.options.sourceFileName,
|
|
63
|
+
this.options.sourceContent
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add a mapping to the source map
|
|
71
|
+
* @param {Object} original - Original position {line, column} (1-based)
|
|
72
|
+
* @param {string} name - Optional identifier name
|
|
73
|
+
*/
|
|
74
|
+
_addMapping(original, name = null) {
|
|
75
|
+
if (!this.sourceMap || !original) return;
|
|
76
|
+
|
|
77
|
+
this.sourceMap.addMapping({
|
|
78
|
+
generated: {
|
|
79
|
+
line: this._currentLine,
|
|
80
|
+
column: this._currentColumn
|
|
81
|
+
},
|
|
82
|
+
original: {
|
|
83
|
+
line: original.line - 1, // Convert to 0-based
|
|
84
|
+
column: original.column - 1
|
|
85
|
+
},
|
|
86
|
+
source: this.options.sourceFileName,
|
|
87
|
+
name
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track output position when writing code
|
|
93
|
+
* @param {string} code - Generated code
|
|
94
|
+
* @returns {string} The same code (for chaining)
|
|
95
|
+
*/
|
|
96
|
+
_trackCode(code) {
|
|
97
|
+
for (const char of code) {
|
|
98
|
+
if (char === '\n') {
|
|
99
|
+
this._currentLine++;
|
|
100
|
+
this._currentColumn = 0;
|
|
101
|
+
} else {
|
|
102
|
+
this._currentColumn++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return code;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Write code with optional source mapping
|
|
110
|
+
* @param {string} code - Code to write
|
|
111
|
+
* @param {Object} original - Original position {line, column}
|
|
112
|
+
* @param {string} name - Optional identifier name
|
|
113
|
+
* @returns {string} The code
|
|
114
|
+
*/
|
|
115
|
+
_emit(code, original = null, name = null) {
|
|
116
|
+
if (original) {
|
|
117
|
+
this._addMapping(original, name);
|
|
118
|
+
}
|
|
119
|
+
return this._trackCode(code);
|
|
39
120
|
}
|
|
40
121
|
|
|
41
122
|
/**
|
|
@@ -100,7 +181,32 @@ export class Transformer {
|
|
|
100
181
|
// Component export
|
|
101
182
|
parts.push(this.generateExport());
|
|
102
183
|
|
|
103
|
-
|
|
184
|
+
const code = parts.filter(Boolean).join('\n\n');
|
|
185
|
+
|
|
186
|
+
// Track the generated code for source map positions
|
|
187
|
+
if (this.sourceMap) {
|
|
188
|
+
this._trackCode(code);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return code;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Transform AST and return result with optional source map
|
|
196
|
+
* @returns {Object} Result with code and optional sourceMap
|
|
197
|
+
*/
|
|
198
|
+
transformWithSourceMap() {
|
|
199
|
+
const code = this.transform();
|
|
200
|
+
|
|
201
|
+
if (!this.sourceMap) {
|
|
202
|
+
return { code, sourceMap: null };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
code,
|
|
207
|
+
sourceMap: this.sourceMap.toJSON(),
|
|
208
|
+
sourceMapComment: this.sourceMap.toComment()
|
|
209
|
+
};
|
|
104
210
|
}
|
|
105
211
|
|
|
106
212
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"default": "./runtime/logger.js"
|
|
43
43
|
},
|
|
44
44
|
"./runtime/hmr": {
|
|
45
|
+
"types": "./types/hmr.d.ts",
|
|
45
46
|
"default": "./runtime/hmr.js"
|
|
46
47
|
},
|
|
47
48
|
"./compiler": {
|
|
@@ -70,8 +71,9 @@
|
|
|
70
71
|
"LICENSE"
|
|
71
72
|
],
|
|
72
73
|
"scripts": {
|
|
73
|
-
"test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze",
|
|
74
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze",
|
|
74
75
|
"test:compiler": "node test/compiler.test.js",
|
|
76
|
+
"test:sourcemap": "node test/sourcemap.test.js",
|
|
75
77
|
"test:pulse": "node test/pulse.test.js",
|
|
76
78
|
"test:dom": "node test/dom.test.js",
|
|
77
79
|
"test:router": "node test/router.test.js",
|
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
|
};
|