pulse-js-framework 1.10.0 → 1.10.3
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/compiler/parser/_extract.js +393 -0
- package/compiler/parser/blocks.js +361 -0
- package/compiler/parser/core.js +306 -0
- package/compiler/parser/expressions.js +386 -0
- package/compiler/parser/imports.js +108 -0
- package/compiler/parser/index.js +47 -0
- package/compiler/parser/state.js +155 -0
- package/compiler/parser/style.js +445 -0
- package/compiler/parser/view.js +632 -0
- package/compiler/parser.js +15 -2372
- package/compiler/parser.js.original +2376 -0
- package/package.json +2 -1
- package/runtime/a11y/announcements.js +213 -0
- package/runtime/a11y/contrast.js +125 -0
- package/runtime/a11y/focus.js +412 -0
- package/runtime/a11y/index.js +35 -0
- package/runtime/a11y/preferences.js +121 -0
- package/runtime/a11y/utils.js +164 -0
- package/runtime/a11y/validation.js +258 -0
- package/runtime/a11y/widgets.js +545 -0
- package/runtime/a11y.js +15 -1840
- package/runtime/a11y.js.original +1844 -0
- package/runtime/graphql/cache.js +69 -0
- package/runtime/graphql/client.js +563 -0
- package/runtime/graphql/hooks.js +492 -0
- package/runtime/graphql/index.js +62 -0
- package/runtime/graphql/subscriptions.js +241 -0
- package/runtime/graphql.js +12 -1322
- package/runtime/graphql.js.original +1326 -0
- package/runtime/router/core.js +956 -0
- package/runtime/router/guards.js +90 -0
- package/runtime/router/history.js +204 -0
- package/runtime/router/index.js +36 -0
- package/runtime/router/lazy.js +180 -0
- package/runtime/router/utils.js +226 -0
- package/runtime/router.js +12 -1600
- package/runtime/router.js.original +1605 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Router - Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for route parsing, matching, and query string handling
|
|
5
|
+
*
|
|
6
|
+
* @module pulse-js-framework/runtime/router/utils
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { loggers } from '../logger.js';
|
|
10
|
+
|
|
11
|
+
const log = loggers.router;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a route pattern into a regex and extract param names
|
|
15
|
+
* Supports: /users/:id, /posts/:id/comments, /files/*path, * (catch-all)
|
|
16
|
+
*/
|
|
17
|
+
export function parsePattern(pattern) {
|
|
18
|
+
const paramNames = [];
|
|
19
|
+
|
|
20
|
+
// Handle standalone * as catch-all
|
|
21
|
+
if (pattern === '*') {
|
|
22
|
+
return {
|
|
23
|
+
regex: /^.*$/,
|
|
24
|
+
paramNames: []
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let regexStr = pattern
|
|
29
|
+
// Escape special regex chars except : and *
|
|
30
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
31
|
+
// Handle wildcard params (*name)
|
|
32
|
+
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
33
|
+
paramNames.push(name);
|
|
34
|
+
return '(.*)';
|
|
35
|
+
})
|
|
36
|
+
// Handle named params (:name)
|
|
37
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
38
|
+
paramNames.push(name);
|
|
39
|
+
return '([^/]+)';
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Ensure exact match
|
|
43
|
+
regexStr = `^${regexStr}$`;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
regex: new RegExp(regexStr),
|
|
47
|
+
paramNames
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize route configuration
|
|
53
|
+
* Supports both simple (handler function) and full (object with meta) definitions
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeRoute(pattern, config) {
|
|
56
|
+
// Simple format: pattern -> handler
|
|
57
|
+
if (typeof config === 'function') {
|
|
58
|
+
return {
|
|
59
|
+
pattern,
|
|
60
|
+
handler: config,
|
|
61
|
+
meta: {},
|
|
62
|
+
beforeEnter: null,
|
|
63
|
+
children: null
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Full format: pattern -> { handler, meta, beforeEnter, children, alias, layout, group }
|
|
68
|
+
return {
|
|
69
|
+
pattern,
|
|
70
|
+
handler: config.handler || config.component,
|
|
71
|
+
meta: config.meta || {},
|
|
72
|
+
beforeEnter: config.beforeEnter || null,
|
|
73
|
+
children: config.children || null,
|
|
74
|
+
redirect: config.redirect || null,
|
|
75
|
+
alias: config.alias || null,
|
|
76
|
+
layout: config.layout || null,
|
|
77
|
+
group: config.group || false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build a query string from an object, supporting arrays and skipping null/undefined
|
|
83
|
+
*
|
|
84
|
+
* @param {Object} query - Query parameters object
|
|
85
|
+
* @returns {string} Encoded query string (without leading ?)
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* buildQueryString({ q: 'hello world', tags: ['a', 'b'] })
|
|
89
|
+
* // 'q=hello+world&tags=a&tags=b'
|
|
90
|
+
*
|
|
91
|
+
* buildQueryString({ a: 'x', b: null, c: undefined })
|
|
92
|
+
* // 'a=x'
|
|
93
|
+
*/
|
|
94
|
+
export function buildQueryString(query) {
|
|
95
|
+
if (!query || typeof query !== 'object') return '';
|
|
96
|
+
|
|
97
|
+
const params = new URLSearchParams();
|
|
98
|
+
|
|
99
|
+
for (const [key, value] of Object.entries(query)) {
|
|
100
|
+
// Skip null and undefined values
|
|
101
|
+
if (value === null || value === undefined) continue;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
// Array values: ?tags=a&tags=b
|
|
105
|
+
for (const item of value) {
|
|
106
|
+
if (item !== null && item !== undefined) {
|
|
107
|
+
params.append(key, String(item));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
params.append(key, String(value));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return params.toString();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Match a path against a route pattern
|
|
120
|
+
*/
|
|
121
|
+
export function matchRoute(pattern, path) {
|
|
122
|
+
const { regex, paramNames } = parsePattern(pattern);
|
|
123
|
+
const match = path.match(regex);
|
|
124
|
+
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
|
|
127
|
+
const params = {};
|
|
128
|
+
paramNames.forEach((name, i) => {
|
|
129
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return params;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Query string validation limits
|
|
136
|
+
const QUERY_LIMITS = {
|
|
137
|
+
maxTotalLength: 2048, // 2KB max for entire query string
|
|
138
|
+
maxValueLength: 1024, // 1KB max per individual value
|
|
139
|
+
maxParams: 50 // Maximum number of query parameters
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse a single query value into its typed representation
|
|
144
|
+
* Only converts when parseQueryTypes is enabled
|
|
145
|
+
*
|
|
146
|
+
* @param {string} value - Raw string value
|
|
147
|
+
* @returns {string|number|boolean} Typed value
|
|
148
|
+
*/
|
|
149
|
+
function parseTypedValue(value) {
|
|
150
|
+
// Boolean detection
|
|
151
|
+
if (value === 'true') return true;
|
|
152
|
+
if (value === 'false') return false;
|
|
153
|
+
|
|
154
|
+
// Number detection (strict: only numeric strings, not hex/octal/empty)
|
|
155
|
+
if (value !== '' && !isNaN(value) && !isNaN(parseFloat(value))) {
|
|
156
|
+
const num = Number(value);
|
|
157
|
+
if (isFinite(num)) return num;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse query string into object with validation
|
|
165
|
+
*
|
|
166
|
+
* SECURITY: Enforces hard limits BEFORE parsing to prevent DoS attacks.
|
|
167
|
+
* - Max total length: 2KB
|
|
168
|
+
* - Max value length: 1KB
|
|
169
|
+
* - Max parameters: 50
|
|
170
|
+
*
|
|
171
|
+
* @param {string} search - Query string (with or without leading ?)
|
|
172
|
+
* @param {Object} [options] - Parsing options
|
|
173
|
+
* @param {boolean} [options.typed=false] - Parse numbers and booleans from string values
|
|
174
|
+
* @returns {Object} Parsed query parameters
|
|
175
|
+
*/
|
|
176
|
+
export function parseQuery(search, options = {}) {
|
|
177
|
+
if (!search) return {};
|
|
178
|
+
|
|
179
|
+
const { typed = false } = options;
|
|
180
|
+
|
|
181
|
+
// Remove leading ? if present
|
|
182
|
+
let queryStr = search.startsWith('?') ? search.slice(1) : search;
|
|
183
|
+
|
|
184
|
+
// SECURITY: Enforce hard limit BEFORE parsing to prevent DoS
|
|
185
|
+
if (queryStr.length > QUERY_LIMITS.maxTotalLength) {
|
|
186
|
+
log.warn(`Query string exceeds maximum length (${QUERY_LIMITS.maxTotalLength} chars). Truncating.`);
|
|
187
|
+
queryStr = queryStr.slice(0, QUERY_LIMITS.maxTotalLength);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const params = new URLSearchParams(queryStr);
|
|
191
|
+
const query = {};
|
|
192
|
+
let paramCount = 0;
|
|
193
|
+
|
|
194
|
+
for (const [key, value] of params) {
|
|
195
|
+
// Check parameter count limit
|
|
196
|
+
if (paramCount >= QUERY_LIMITS.maxParams) {
|
|
197
|
+
log.warn(`Query string exceeds maximum parameters (${QUERY_LIMITS.maxParams}). Ignoring excess.`);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate and potentially truncate value length
|
|
202
|
+
let safeValue = value;
|
|
203
|
+
if (value.length > QUERY_LIMITS.maxValueLength) {
|
|
204
|
+
log.warn(`Query parameter "${key}" exceeds maximum length. Truncating.`);
|
|
205
|
+
safeValue = value.slice(0, QUERY_LIMITS.maxValueLength);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply typed parsing if enabled
|
|
209
|
+
if (typed) {
|
|
210
|
+
safeValue = parseTypedValue(safeValue);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (key in query) {
|
|
214
|
+
// Multiple values for same key
|
|
215
|
+
if (Array.isArray(query[key])) {
|
|
216
|
+
query[key].push(safeValue);
|
|
217
|
+
} else {
|
|
218
|
+
query[key] = [query[key], safeValue];
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
query[key] = safeValue;
|
|
222
|
+
}
|
|
223
|
+
paramCount++;
|
|
224
|
+
}
|
|
225
|
+
return query;
|
|
226
|
+
}
|