pulse-js-framework 1.7.8 → 1.7.10
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/cli/lint.js +442 -3
- package/compiler/lexer.js +6 -0
- package/compiler/parser.js +144 -1
- package/compiler/transformer/imports.js +15 -0
- package/compiler/transformer/index.js +46 -0
- package/compiler/transformer/view.js +180 -5
- package/package.json +14 -2
- package/runtime/a11y.js +1005 -0
- package/runtime/devtools/a11y-audit.js +442 -0
- package/runtime/devtools/diagnostics.js +403 -0
- package/runtime/devtools/index.js +53 -0
- package/runtime/devtools/time-travel.js +189 -0
- package/runtime/devtools.js +138 -497
- package/runtime/dom-binding.js +7 -4
- package/runtime/dom-element.js +192 -1
- package/runtime/dom.js +8 -2
- package/runtime/http.js +837 -0
- package/runtime/index.js +2 -0
- package/runtime/native.js +2 -2
- package/runtime/security.js +461 -0
- package/runtime/utils.js +37 -16
- package/types/a11y.d.ts +336 -0
|
@@ -54,6 +54,13 @@ export class Transformer {
|
|
|
54
54
|
this.importedComponents = new Map();
|
|
55
55
|
this.scopeId = this.options.scopeStyles ? generateScopeId() : null;
|
|
56
56
|
|
|
57
|
+
// Track a11y feature usage for conditional imports
|
|
58
|
+
this.usesA11y = {
|
|
59
|
+
srOnly: false,
|
|
60
|
+
trapFocus: false,
|
|
61
|
+
announce: false
|
|
62
|
+
};
|
|
63
|
+
|
|
57
64
|
// Source map tracking
|
|
58
65
|
this.sourceMap = null;
|
|
59
66
|
this._currentLine = 0;
|
|
@@ -126,6 +133,40 @@ export class Transformer {
|
|
|
126
133
|
return this._trackCode(code);
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Pre-scan AST for a11y directive usage
|
|
138
|
+
*/
|
|
139
|
+
_scanA11yUsage(node) {
|
|
140
|
+
if (!node) return;
|
|
141
|
+
|
|
142
|
+
// Check directives for a11y usage
|
|
143
|
+
if (node.directives) {
|
|
144
|
+
for (const directive of node.directives) {
|
|
145
|
+
if (directive.type === 'A11yDirective') {
|
|
146
|
+
if (directive.attrs && directive.attrs.srOnly) {
|
|
147
|
+
this.usesA11y.srOnly = true;
|
|
148
|
+
}
|
|
149
|
+
} else if (directive.type === 'FocusTrapDirective') {
|
|
150
|
+
this.usesA11y.trapFocus = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Recursively scan children
|
|
156
|
+
if (node.children) {
|
|
157
|
+
for (const child of node.children) {
|
|
158
|
+
this._scanA11yUsage(child);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Scan view block children
|
|
163
|
+
if (node.type === 'ViewBlock' && node.children) {
|
|
164
|
+
for (const child of node.children) {
|
|
165
|
+
this._scanA11yUsage(child);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
129
170
|
/**
|
|
130
171
|
* Transform AST to JavaScript code
|
|
131
172
|
*/
|
|
@@ -137,6 +178,11 @@ export class Transformer {
|
|
|
137
178
|
extractImportedComponents(this, this.ast.imports);
|
|
138
179
|
}
|
|
139
180
|
|
|
181
|
+
// Pre-scan for a11y usage to determine imports
|
|
182
|
+
if (this.ast.view) {
|
|
183
|
+
this._scanA11yUsage(this.ast.view);
|
|
184
|
+
}
|
|
185
|
+
|
|
140
186
|
// Imports (runtime + user imports)
|
|
141
187
|
parts.push(generateImports(this));
|
|
142
188
|
|
|
@@ -18,7 +18,11 @@ export const VIEW_NODE_HANDLERS = {
|
|
|
18
18
|
[NodeType.SlotElement]: 'transformSlot',
|
|
19
19
|
[NodeType.LinkDirective]: 'transformLinkDirective',
|
|
20
20
|
[NodeType.OutletDirective]: 'transformOutletDirective',
|
|
21
|
-
[NodeType.NavigateDirective]: 'transformNavigateDirective'
|
|
21
|
+
[NodeType.NavigateDirective]: 'transformNavigateDirective',
|
|
22
|
+
// Accessibility directives
|
|
23
|
+
[NodeType.A11yDirective]: 'transformA11yDirective',
|
|
24
|
+
[NodeType.LiveDirective]: 'transformLiveDirective',
|
|
25
|
+
[NodeType.FocusTrapDirective]: 'transformFocusTrapDirective'
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -93,6 +97,12 @@ export function transformViewNode(transformer, node, indent = 0) {
|
|
|
93
97
|
return transformOutletDirective(transformer, node, indent);
|
|
94
98
|
case NodeType.NavigateDirective:
|
|
95
99
|
return transformNavigateDirective(transformer, node, indent);
|
|
100
|
+
case NodeType.A11yDirective:
|
|
101
|
+
return transformA11yDirective(transformer, node, indent);
|
|
102
|
+
case NodeType.LiveDirective:
|
|
103
|
+
return transformLiveDirective(transformer, node, indent);
|
|
104
|
+
case NodeType.FocusTrapDirective:
|
|
105
|
+
return transformFocusTrapDirective(transformer, node, indent);
|
|
96
106
|
default:
|
|
97
107
|
return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
|
|
98
108
|
}
|
|
@@ -192,6 +202,99 @@ export function transformNavigateDirective(transformer, node, indent) {
|
|
|
192
202
|
return `${pad}router.navigate(${path}${options})`;
|
|
193
203
|
}
|
|
194
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Transform @a11y directive - sets ARIA attributes
|
|
207
|
+
* @param {Object} transformer - Transformer instance
|
|
208
|
+
* @param {Object} node - A11y directive node
|
|
209
|
+
* @param {number} indent - Indentation level
|
|
210
|
+
* @returns {string} JavaScript code
|
|
211
|
+
*/
|
|
212
|
+
export function transformA11yDirective(transformer, node, indent) {
|
|
213
|
+
const pad = ' '.repeat(indent);
|
|
214
|
+
const attrs = node.attrs || {};
|
|
215
|
+
|
|
216
|
+
// Handle @srOnly - create visually hidden element
|
|
217
|
+
if (attrs.srOnly) {
|
|
218
|
+
return `${pad}srOnly(/* content */)`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Build ARIA attributes object
|
|
222
|
+
const ariaAttrs = Object.entries(attrs).map(([key, value]) => {
|
|
223
|
+
// Map short names to aria- attributes (role is not prefixed)
|
|
224
|
+
const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
|
|
225
|
+
const valueCode = typeof value === 'string' ? `'${value}'` : transformExpression(transformer, value);
|
|
226
|
+
return `'${ariaKey}': ${valueCode}`;
|
|
227
|
+
}).join(', ');
|
|
228
|
+
|
|
229
|
+
return `{ ${ariaAttrs} }`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build ARIA attributes object from a11y directive
|
|
234
|
+
* @param {Object} transformer - Transformer instance
|
|
235
|
+
* @param {Object} directive - A11y directive node
|
|
236
|
+
* @returns {Object} Object with key-value pairs for ARIA attributes
|
|
237
|
+
*/
|
|
238
|
+
export function buildA11yAttributes(transformer, directive) {
|
|
239
|
+
const attrs = directive.attrs || {};
|
|
240
|
+
const result = {};
|
|
241
|
+
|
|
242
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
243
|
+
if (key === 'srOnly') continue;
|
|
244
|
+
// Map short names to aria- attributes (role is not prefixed)
|
|
245
|
+
const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
|
|
246
|
+
|
|
247
|
+
// Handle different value types
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
result[ariaKey] = value;
|
|
250
|
+
} else if (typeof value === 'boolean') {
|
|
251
|
+
result[ariaKey] = String(value); // Convert true/false to "true"/"false"
|
|
252
|
+
} else if (typeof value === 'number') {
|
|
253
|
+
result[ariaKey] = String(value);
|
|
254
|
+
} else {
|
|
255
|
+
result[ariaKey] = transformExpression(transformer, value);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Transform @live directive - creates live region
|
|
264
|
+
* @param {Object} transformer - Transformer instance
|
|
265
|
+
* @param {Object} node - Live directive node
|
|
266
|
+
* @param {number} indent - Indentation level
|
|
267
|
+
* @returns {string} JavaScript code
|
|
268
|
+
*/
|
|
269
|
+
export function transformLiveDirective(transformer, node, indent) {
|
|
270
|
+
const priority = node.priority || 'polite';
|
|
271
|
+
return `'${priority}'`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Transform @focusTrap directive
|
|
276
|
+
* @param {Object} transformer - Transformer instance
|
|
277
|
+
* @param {Object} node - Focus trap directive node
|
|
278
|
+
* @param {number} indent - Indentation level
|
|
279
|
+
* @returns {string} JavaScript code
|
|
280
|
+
*/
|
|
281
|
+
export function transformFocusTrapDirective(transformer, node, indent) {
|
|
282
|
+
const options = node.options || {};
|
|
283
|
+
|
|
284
|
+
if (Object.keys(options).length === 0) {
|
|
285
|
+
return '{}';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const optionsCode = Object.entries(options).map(([key, value]) => {
|
|
289
|
+
const valueCode = typeof value === 'boolean' ? String(value) :
|
|
290
|
+
typeof value === 'string' ? `'${value}'` :
|
|
291
|
+
transformExpression(transformer, value);
|
|
292
|
+
return `${key}: ${valueCode}`;
|
|
293
|
+
}).join(', ');
|
|
294
|
+
|
|
295
|
+
return `{ ${optionsCode} }`;
|
|
296
|
+
}
|
|
297
|
+
|
|
195
298
|
/**
|
|
196
299
|
* Transform element
|
|
197
300
|
* @param {Object} transformer - Transformer instance
|
|
@@ -220,11 +323,77 @@ export function transformElement(transformer, node, indent) {
|
|
|
220
323
|
selector = addScopeToSelector(transformer, selector);
|
|
221
324
|
}
|
|
222
325
|
|
|
223
|
-
//
|
|
224
|
-
parts.push(`${pad}el('${selector}'`);
|
|
225
|
-
|
|
226
|
-
// Add event handlers as on() chain
|
|
326
|
+
// Extract directives by type
|
|
227
327
|
const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
|
|
328
|
+
const a11yDirectives = node.directives.filter(d => d.type === NodeType.A11yDirective);
|
|
329
|
+
const liveDirectives = node.directives.filter(d => d.type === NodeType.LiveDirective);
|
|
330
|
+
const focusTrapDirectives = node.directives.filter(d => d.type === NodeType.FocusTrapDirective);
|
|
331
|
+
|
|
332
|
+
// Check for @srOnly directive
|
|
333
|
+
const srOnlyDirective = a11yDirectives.find(d => d.attrs && d.attrs.srOnly);
|
|
334
|
+
|
|
335
|
+
// If @srOnly, wrap entire content
|
|
336
|
+
if (srOnlyDirective) {
|
|
337
|
+
transformer.usesA11y.srOnly = true;
|
|
338
|
+
const content = [];
|
|
339
|
+
for (const text of node.textContent) {
|
|
340
|
+
content.push(transformTextNode(transformer, text, 0).trim());
|
|
341
|
+
}
|
|
342
|
+
for (const child of node.children) {
|
|
343
|
+
content.push(transformViewNode(transformer, child, 0).trim());
|
|
344
|
+
}
|
|
345
|
+
const contentCode = content.length === 1 ? content[0] : `[${content.join(', ')}]`;
|
|
346
|
+
return `${pad}srOnly(${contentCode})`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Track focusTrap usage
|
|
350
|
+
if (focusTrapDirectives.length > 0) {
|
|
351
|
+
transformer.usesA11y.trapFocus = true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Build ARIA attributes from directives
|
|
355
|
+
const ariaAttrs = [];
|
|
356
|
+
|
|
357
|
+
// Process @a11y directives
|
|
358
|
+
for (const directive of a11yDirectives) {
|
|
359
|
+
const attrs = buildA11yAttributes(transformer, directive);
|
|
360
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
361
|
+
const valueCode = typeof value === 'string' ? `'${value}'` : value;
|
|
362
|
+
ariaAttrs.push(`'${key}': ${valueCode}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Process @live directives (add aria-live and aria-atomic)
|
|
367
|
+
for (const directive of liveDirectives) {
|
|
368
|
+
const priority = directive.priority || 'polite';
|
|
369
|
+
ariaAttrs.push(`'aria-live': '${priority}'`);
|
|
370
|
+
ariaAttrs.push(`'aria-atomic': 'true'`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Build selector with inline ARIA attributes
|
|
374
|
+
let enhancedSelector = selector;
|
|
375
|
+
if (ariaAttrs.length > 0) {
|
|
376
|
+
// Convert ARIA attrs to selector attribute syntax where possible
|
|
377
|
+
// For dynamic values, we'll need to use setAriaAttributes
|
|
378
|
+
const staticAttrs = [];
|
|
379
|
+
const dynamicAttrs = [];
|
|
380
|
+
|
|
381
|
+
for (const attr of ariaAttrs) {
|
|
382
|
+
const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
|
|
383
|
+
if (match) {
|
|
384
|
+
// Static attribute - can embed in selector
|
|
385
|
+
staticAttrs.push(`[${match[1]}=${match[2]}]`);
|
|
386
|
+
} else {
|
|
387
|
+
dynamicAttrs.push(attr);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Add static ARIA attributes to selector
|
|
392
|
+
enhancedSelector = selector + staticAttrs.join('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Start with el() call
|
|
396
|
+
parts.push(`${pad}el('${enhancedSelector}'`);
|
|
228
397
|
|
|
229
398
|
// Add text content
|
|
230
399
|
if (node.textContent.length > 0) {
|
|
@@ -251,6 +420,12 @@ export function transformElement(transformer, node, indent) {
|
|
|
251
420
|
result = `on(${result}, '${handler.event}', () => { ${handlerCode}; })`;
|
|
252
421
|
}
|
|
253
422
|
|
|
423
|
+
// Chain focus trap if present
|
|
424
|
+
for (const directive of focusTrapDirectives) {
|
|
425
|
+
const optionsCode = transformFocusTrapDirective(transformer, directive, 0);
|
|
426
|
+
result = `trapFocus(${result}, ${optionsCode})`;
|
|
427
|
+
}
|
|
428
|
+
|
|
254
429
|
return result;
|
|
255
430
|
}
|
|
256
431
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.10",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -64,6 +64,14 @@
|
|
|
64
64
|
"types": "./types/form.d.ts",
|
|
65
65
|
"default": "./runtime/form.js"
|
|
66
66
|
},
|
|
67
|
+
"./runtime/http": {
|
|
68
|
+
"types": "./types/http.d.ts",
|
|
69
|
+
"default": "./runtime/http.js"
|
|
70
|
+
},
|
|
71
|
+
"./runtime/a11y": {
|
|
72
|
+
"types": "./types/a11y.d.ts",
|
|
73
|
+
"default": "./runtime/a11y.js"
|
|
74
|
+
},
|
|
67
75
|
"./runtime/devtools": "./runtime/devtools.js",
|
|
68
76
|
"./compiler": {
|
|
69
77
|
"types": "./types/index.d.ts",
|
|
@@ -93,7 +101,7 @@
|
|
|
93
101
|
"LICENSE"
|
|
94
102
|
],
|
|
95
103
|
"scripts": {
|
|
96
|
-
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:devtools && npm run test:native",
|
|
104
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors",
|
|
97
105
|
"test:compiler": "node test/compiler.test.js",
|
|
98
106
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
99
107
|
"test:pulse": "node test/pulse.test.js",
|
|
@@ -110,8 +118,12 @@
|
|
|
110
118
|
"test:docs": "node test/docs.test.js",
|
|
111
119
|
"test:async": "node test/async.test.js",
|
|
112
120
|
"test:form": "node test/form.test.js",
|
|
121
|
+
"test:http": "node test/http.test.js",
|
|
113
122
|
"test:devtools": "node test/devtools.test.js",
|
|
114
123
|
"test:native": "node test/native.test.js",
|
|
124
|
+
"test:a11y": "node test/a11y.test.js",
|
|
125
|
+
"test:logger": "node test/logger.test.js",
|
|
126
|
+
"test:errors": "node test/errors.test.js",
|
|
115
127
|
"build:netlify": "node scripts/build-netlify.js",
|
|
116
128
|
"version": "node scripts/sync-version.js",
|
|
117
129
|
"docs": "node cli/index.js dev docs"
|