pulse-js-framework 1.11.3 → 1.11.4
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/analyze.js +21 -8
- package/cli/build.js +83 -56
- package/cli/dev.js +108 -94
- package/cli/docs-test.js +52 -33
- package/cli/index.js +81 -51
- package/cli/mobile.js +92 -40
- package/cli/release.js +64 -46
- package/cli/scaffold.js +14 -13
- package/compiler/lexer.js +55 -54
- package/compiler/parser/core.js +1 -0
- package/compiler/parser/state.js +6 -12
- package/compiler/parser/style.js +17 -20
- package/compiler/parser/view.js +1 -3
- package/compiler/preprocessor.js +124 -262
- package/compiler/sourcemap.js +10 -4
- package/compiler/transformer/expressions.js +122 -106
- package/compiler/transformer/index.js +2 -4
- package/compiler/transformer/style.js +74 -7
- package/compiler/transformer/view.js +86 -36
- package/loader/esbuild-plugin-server-components.js +209 -0
- package/loader/esbuild-plugin.js +41 -93
- package/loader/parcel-plugin.js +37 -97
- package/loader/rollup-plugin-server-components.js +30 -169
- package/loader/rollup-plugin.js +27 -78
- package/loader/shared.js +362 -0
- package/loader/swc-plugin.js +65 -82
- package/loader/vite-plugin-server-components.js +30 -171
- package/loader/vite-plugin.js +25 -10
- package/loader/webpack-loader-server-components.js +21 -134
- package/loader/webpack-loader.js +25 -80
- package/package.json +52 -12
- package/runtime/dom-selector.js +2 -1
- package/runtime/form.js +4 -3
- package/runtime/http.js +6 -1
- package/runtime/logger.js +44 -24
- package/runtime/router/utils.js +14 -7
- package/runtime/security.js +13 -1
- package/runtime/server-components/actions-server.js +23 -19
- package/runtime/server-components/error-sanitizer.js +18 -18
- package/runtime/server-components/security.js +41 -24
- package/runtime/ssr-preload.js +5 -3
- package/runtime/testing.js +759 -0
- package/runtime/utils.js +3 -2
- package/server/utils.js +15 -9
- package/sw/index.js +2 -0
- package/types/loaders.d.ts +1043 -0
- package/compiler/parser/_extract.js +0 -393
- package/loader/README.md +0 -509
|
@@ -14,6 +14,15 @@ import {
|
|
|
14
14
|
STATEMENT_END_TYPES
|
|
15
15
|
} from './constants.js';
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Escape special regex characters in a string
|
|
19
|
+
* @param {string} str - String to escape
|
|
20
|
+
* @returns {string} Escaped string safe for use in RegExp
|
|
21
|
+
*/
|
|
22
|
+
function escapeRegExp(str) {
|
|
23
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
/**
|
|
18
27
|
* Transform AST expression to JS code
|
|
19
28
|
* @param {Object} transformer - Transformer instance
|
|
@@ -186,18 +195,26 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
186
195
|
// Both are now reactive (useProp returns computed for uniform interface)
|
|
187
196
|
let result = exprStr;
|
|
188
197
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
// Build combined patterns once for all variables (O(1) regex compilations instead of O(n))
|
|
199
|
+
const stateVarsArr = [...transformer.stateVars];
|
|
200
|
+
const propVarsArr = [...transformer.propVars];
|
|
201
|
+
|
|
202
|
+
if (stateVarsArr.length > 0) {
|
|
203
|
+
const stateAlt = stateVarsArr.map(escapeRegExp).join('|');
|
|
204
|
+
|
|
205
|
+
// First, handle assignments to state vars: stateVar = expr -> stateVar.set(expr)
|
|
206
|
+
// This must happen before the generic .get() replacement to avoid generating
|
|
207
|
+
// invalid code like stateVar.get() = expr (LHS of assignment is not a reference)
|
|
208
|
+
|
|
193
209
|
// Compound assignment: stateVar += expr -> stateVar.update(_v => _v + expr)
|
|
194
|
-
//
|
|
195
|
-
const compoundPattern = new RegExp(`\\b${
|
|
210
|
+
// Single combined regex for all state vars
|
|
211
|
+
const compoundPattern = new RegExp(`\\b(${stateAlt})\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g');
|
|
196
212
|
let compoundMatch;
|
|
197
213
|
const compoundReplacements = [];
|
|
198
214
|
|
|
199
215
|
while ((compoundMatch = compoundPattern.exec(result)) !== null) {
|
|
200
|
-
const
|
|
216
|
+
const varName = compoundMatch[1];
|
|
217
|
+
const op = compoundMatch[2];
|
|
201
218
|
const baseOp = op.slice(0, -1);
|
|
202
219
|
const rhsStart = compoundMatch.index + compoundMatch[0].length;
|
|
203
220
|
|
|
@@ -220,7 +237,7 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
220
237
|
compoundReplacements.push({
|
|
221
238
|
start: compoundMatch.index,
|
|
222
239
|
end: endIdx,
|
|
223
|
-
replacement: `${
|
|
240
|
+
replacement: `${varName}.update(_v => _v ${baseOp} ${rhs})`
|
|
224
241
|
});
|
|
225
242
|
}
|
|
226
243
|
}
|
|
@@ -232,12 +249,13 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
// Simple assignment: stateVar = expr -> stateVar.set(expr)
|
|
235
|
-
//
|
|
236
|
-
const simplePattern = new RegExp(`\\b${
|
|
252
|
+
// Single combined regex for all state vars
|
|
253
|
+
const simplePattern = new RegExp(`\\b(${stateAlt})\\s*=(?!=)\\s*`, 'g');
|
|
237
254
|
let simpleMatch;
|
|
238
255
|
const simpleReplacements = [];
|
|
239
256
|
|
|
240
257
|
while ((simpleMatch = simplePattern.exec(result)) !== null) {
|
|
258
|
+
const varName = simpleMatch[1];
|
|
241
259
|
const rhsStart = simpleMatch.index + simpleMatch[0].length;
|
|
242
260
|
|
|
243
261
|
let depth = 0;
|
|
@@ -258,7 +276,7 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
258
276
|
simpleReplacements.push({
|
|
259
277
|
start: simpleMatch.index,
|
|
260
278
|
end: endIdx,
|
|
261
|
-
replacement: `${
|
|
279
|
+
replacement: `${varName}.set(${rhs})`
|
|
262
280
|
});
|
|
263
281
|
}
|
|
264
282
|
}
|
|
@@ -268,31 +286,31 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
268
286
|
const r = simpleReplacements[i];
|
|
269
287
|
result = result.slice(0, r.start) + r.replacement + result.slice(r.end);
|
|
270
288
|
}
|
|
271
|
-
}
|
|
272
289
|
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
// Transform state var reads (not already transformed to .get/.set/.update)
|
|
291
|
+
// Single combined regex for all state vars
|
|
275
292
|
result = result.replace(
|
|
276
|
-
new RegExp(`\\b${
|
|
277
|
-
|
|
293
|
+
new RegExp(`\\b(${stateAlt})\\b(?!\\.(?:get|set|update))`, 'g'),
|
|
294
|
+
'$1.get()'
|
|
278
295
|
);
|
|
279
296
|
}
|
|
280
297
|
|
|
281
298
|
// Transform prop vars (now also reactive via useProp)
|
|
282
299
|
// Add optional chaining when followed by property access for nullable props
|
|
283
300
|
// Props commonly receive null values (e.g., notification: null)
|
|
284
|
-
|
|
301
|
+
if (propVarsArr.length > 0) {
|
|
302
|
+
const propAlt = propVarsArr.map(escapeRegExp).join('|');
|
|
285
303
|
// Property access: propVar.x -> propVar.get()?.x
|
|
286
304
|
// Guard against already-transformed: skip if followed by .get( or .set(
|
|
287
305
|
result = result.replace(
|
|
288
|
-
new RegExp(`\\b${
|
|
289
|
-
|
|
306
|
+
new RegExp(`\\b(${propAlt})\\b(?=\\.(?!get\\(|set\\())`, 'g'),
|
|
307
|
+
'$1.get()?'
|
|
290
308
|
);
|
|
291
309
|
// Handle standalone prop var (not followed by property access)
|
|
292
310
|
// Guard: skip if already followed by .get or .set
|
|
293
311
|
result = result.replace(
|
|
294
|
-
new RegExp(`\\b${
|
|
295
|
-
|
|
312
|
+
new RegExp(`\\b(${propAlt})\\b(?!\\.(?:get|set)\\()(?!\\.)`, 'g'),
|
|
313
|
+
'$1.get()'
|
|
296
314
|
);
|
|
297
315
|
}
|
|
298
316
|
|
|
@@ -420,12 +438,30 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
420
438
|
// Protect string literals from state var replacement
|
|
421
439
|
const stringPlaceholders = [];
|
|
422
440
|
const protectStrings = (str) => {
|
|
423
|
-
//
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
441
|
+
// Linear scan to find and replace string/template literals with placeholders.
|
|
442
|
+
// Avoids regex to prevent any ReDoS risk from adversarial input.
|
|
443
|
+
let result = '';
|
|
444
|
+
let i = 0;
|
|
445
|
+
while (i < str.length) {
|
|
446
|
+
const ch = str[i];
|
|
447
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
448
|
+
const quote = ch;
|
|
449
|
+
let j = i + 1;
|
|
450
|
+
while (j < str.length) {
|
|
451
|
+
if (str[j] === '\\') { j += 2; continue; }
|
|
452
|
+
if (str[j] === quote) { j++; break; }
|
|
453
|
+
j++;
|
|
454
|
+
}
|
|
455
|
+
const index = stringPlaceholders.length;
|
|
456
|
+
stringPlaceholders.push(str.slice(i, j));
|
|
457
|
+
result += `__STRING_${index}__`;
|
|
458
|
+
i = j;
|
|
459
|
+
} else {
|
|
460
|
+
result += ch;
|
|
461
|
+
i++;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
429
465
|
};
|
|
430
466
|
const restoreStrings = (str) => {
|
|
431
467
|
return str.replace(/__STRING_(\d+)__/g, (_, index) => stringPlaceholders[parseInt(index)]);
|
|
@@ -434,19 +470,27 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
434
470
|
// Protect strings before transformations
|
|
435
471
|
code = protectStrings(code);
|
|
436
472
|
|
|
437
|
-
// Build patterns for state variable transformation
|
|
438
|
-
const stateVarPattern = [...stateVars].join('|');
|
|
439
|
-
const funcPattern = [...actionNames, ...BUILTIN_FUNCTIONS].join('|');
|
|
440
|
-
const keywordsPattern = [...STATEMENT_KEYWORDS].join('|');
|
|
473
|
+
// Build patterns for state variable transformation (precompiled once)
|
|
474
|
+
const stateVarPattern = [...stateVars].map(escapeRegExp).join('|');
|
|
475
|
+
const funcPattern = [...actionNames, ...BUILTIN_FUNCTIONS].map(escapeRegExp).join('|');
|
|
476
|
+
const keywordsPattern = [...STATEMENT_KEYWORDS].map(escapeRegExp).join('|');
|
|
477
|
+
|
|
478
|
+
// Precompile boundary regex once (was O(n²) when created inside inner loop)
|
|
479
|
+
const keywordBoundary = new RegExp(`^\\s+(?:(?:${stateVarPattern})\\s*=(?!=)|(?:${keywordsPattern}|await|return)\\b|(?:${funcPattern})\\s*\\()`);
|
|
480
|
+
|
|
481
|
+
// Combined assignment pattern for all state vars (single regex instead of N)
|
|
482
|
+
const assignPattern = stateVars.size > 0
|
|
483
|
+
? new RegExp(`\\b(${stateVarPattern})\\s*=(?!=)`, 'g')
|
|
484
|
+
: null;
|
|
441
485
|
|
|
442
486
|
// Transform state var assignments: stateVar = value -> stateVar.set(value)
|
|
443
487
|
// Match assignment and find end by tracking balanced brackets
|
|
444
|
-
|
|
445
|
-
const pattern = new RegExp(`\\b${stateVar}\\s*=(?!=)`, 'g');
|
|
488
|
+
if (assignPattern) {
|
|
446
489
|
let match;
|
|
447
490
|
const replacements = [];
|
|
448
491
|
|
|
449
|
-
while ((match =
|
|
492
|
+
while ((match = assignPattern.exec(code)) !== null) {
|
|
493
|
+
const stateVar = match[1];
|
|
450
494
|
const startIdx = match.index + match[0].length;
|
|
451
495
|
|
|
452
496
|
// Skip whitespace
|
|
@@ -508,7 +552,6 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
508
552
|
// Check for whitespace followed by keyword/identifier that starts a new statement
|
|
509
553
|
if (/\s/.test(ch)) {
|
|
510
554
|
const rest = code.slice(i);
|
|
511
|
-
const keywordBoundary = new RegExp(`^\\s+(?:(?:${stateVarPattern})\\s*=(?!=)|(?:${keywordsPattern}|await|return)\\b|(?:${funcPattern})\\s*\\()`);
|
|
512
555
|
if (keywordBoundary.test(rest)) {
|
|
513
556
|
break;
|
|
514
557
|
}
|
|
@@ -539,99 +582,72 @@ export function transformFunctionBody(transformer, tokens) {
|
|
|
539
582
|
code = code.replace(/;+/g, ';');
|
|
540
583
|
code = code.replace(/; ;/g, ';');
|
|
541
584
|
|
|
542
|
-
// Handle post-increment/decrement on state vars
|
|
543
|
-
for (
|
|
585
|
+
// Handle post-increment/decrement on state vars
|
|
586
|
+
// Combined regex for all state vars (single pass instead of 4N passes)
|
|
587
|
+
if (stateVarPattern) {
|
|
544
588
|
// Post-increment: stateVar++ (returns old value)
|
|
545
589
|
code = code.replace(
|
|
546
|
-
new RegExp(`\\b${
|
|
547
|
-
`((v) => (${
|
|
590
|
+
new RegExp(`\\b(${stateVarPattern})\\s*\\+\\+`, 'g'),
|
|
591
|
+
(_, v) => `((v) => (${v}.set(v + 1), v))(${v}.get())`
|
|
548
592
|
);
|
|
549
593
|
// Post-decrement: stateVar-- (returns old value)
|
|
550
594
|
code = code.replace(
|
|
551
|
-
new RegExp(`\\b${
|
|
552
|
-
`((v) => (${
|
|
595
|
+
new RegExp(`\\b(${stateVarPattern})\\s*--`, 'g'),
|
|
596
|
+
(_, v) => `((v) => (${v}.set(v - 1), v))(${v}.get())`
|
|
553
597
|
);
|
|
554
598
|
// Pre-increment: ++stateVar (returns new value)
|
|
555
599
|
code = code.replace(
|
|
556
|
-
new RegExp(`\\+\\+\\s
|
|
557
|
-
`(${
|
|
600
|
+
new RegExp(`\\+\\+\\s*(${stateVarPattern})\\b`, 'g'),
|
|
601
|
+
(_, v) => `(${v}.set(${v}.get() + 1), ${v}.get())`
|
|
558
602
|
);
|
|
559
603
|
// Pre-decrement: --stateVar (returns new value)
|
|
560
604
|
code = code.replace(
|
|
561
|
-
new RegExp(`--\\s
|
|
562
|
-
`(${
|
|
605
|
+
new RegExp(`--\\s*(${stateVarPattern})\\b`, 'g'),
|
|
606
|
+
(_, v) => `(${v}.set(${v}.get() - 1), ${v}.get())`
|
|
563
607
|
);
|
|
564
608
|
}
|
|
565
609
|
|
|
610
|
+
// Helper: check if match at offset is an object key (shared by state/prop reads)
|
|
611
|
+
const isObjectKey = (str, match, offset) => {
|
|
612
|
+
const after = str.slice(offset + match.length, offset + match.length + 10);
|
|
613
|
+
if (!/^\s*:(?!:)/.test(after)) return false;
|
|
614
|
+
let depth = 0;
|
|
615
|
+
for (let i = offset - 1; i >= 0; i--) {
|
|
616
|
+
const ch = str[i];
|
|
617
|
+
if (ch === ')' || ch === ']') depth++;
|
|
618
|
+
else if (ch === '(' || ch === '[') depth--;
|
|
619
|
+
else if (ch === '}') depth++;
|
|
620
|
+
else if (ch === '{') {
|
|
621
|
+
if (depth === 0) return true;
|
|
622
|
+
depth--;
|
|
623
|
+
}
|
|
624
|
+
else if (ch === ',' && depth === 0) return true;
|
|
625
|
+
else if (ch === ';' && depth === 0) break;
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
628
|
+
};
|
|
629
|
+
|
|
566
630
|
// Replace state var reads (not in assignments, not already with .get/.set)
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
for (const stateVar of stateVars) {
|
|
631
|
+
// Combined regex for all state vars (single pass instead of N passes)
|
|
632
|
+
if (stateVarPattern) {
|
|
570
633
|
code = code.replace(
|
|
571
|
-
new RegExp(`(?:(?<=\\.\\.\\.)|(?<!\\.))\\b${
|
|
572
|
-
(match, offset) => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const after = code.slice(offset + match.length, offset + match.length + 10);
|
|
576
|
-
|
|
577
|
-
// If followed by : (not ::), check if it's an object key
|
|
578
|
-
if (/^\s*:(?!:)/.test(after)) {
|
|
579
|
-
// Look backwards for the nearest { or , that would indicate object context
|
|
580
|
-
// We need to track bracket depth to handle nested structures
|
|
581
|
-
let depth = 0;
|
|
582
|
-
for (let i = offset - 1; i >= 0; i--) {
|
|
583
|
-
const ch = code[i];
|
|
584
|
-
if (ch === ')' || ch === ']') depth++;
|
|
585
|
-
else if (ch === '(' || ch === '[') depth--;
|
|
586
|
-
else if (ch === '}') depth++;
|
|
587
|
-
else if (ch === '{') {
|
|
588
|
-
if (depth === 0) {
|
|
589
|
-
// Found opening brace at same depth - this is an object key
|
|
590
|
-
return match;
|
|
591
|
-
}
|
|
592
|
-
depth--;
|
|
593
|
-
}
|
|
594
|
-
else if (ch === ',' && depth === 0) {
|
|
595
|
-
// Found comma at same depth - this is an object key
|
|
596
|
-
return match;
|
|
597
|
-
}
|
|
598
|
-
// Stop if we hit a semicolon at depth 0 (different statement)
|
|
599
|
-
else if (ch === ';' && depth === 0) break;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return `${stateVar}.get()`;
|
|
634
|
+
new RegExp(`(?:(?<=\\.\\.\\.)|(?<!\\.))\\b(${stateVarPattern})\\b(?!\\s*=(?!=)|\\s*\\(|\\s*\\.(?:get|set))`, 'g'),
|
|
635
|
+
(match, varName, offset) => {
|
|
636
|
+
if (isObjectKey(code, match, offset)) return match;
|
|
637
|
+
return `${varName}.get()`;
|
|
604
638
|
}
|
|
605
639
|
);
|
|
606
640
|
}
|
|
607
641
|
|
|
608
642
|
// Replace prop var reads (props are reactive via useProp, need .get() like state vars)
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
643
|
+
// Combined regex for all prop vars (single pass instead of N passes)
|
|
644
|
+
if (propVars.size > 0) {
|
|
645
|
+
const propVarPattern = [...propVars].map(escapeRegExp).join('|');
|
|
612
646
|
code = code.replace(
|
|
613
|
-
new RegExp(`(?:(?<=\\.\\.\\.)|(?<!\\.))\\b${
|
|
614
|
-
(match, offset) => {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if (/^\s*:(?!:)/.test(after)) {
|
|
619
|
-
let depth = 0;
|
|
620
|
-
for (let i = offset - 1; i >= 0; i--) {
|
|
621
|
-
const ch = code[i];
|
|
622
|
-
if (ch === ')' || ch === ']') depth++;
|
|
623
|
-
else if (ch === '(' || ch === '[') depth--;
|
|
624
|
-
else if (ch === '}') depth++;
|
|
625
|
-
else if (ch === '{') {
|
|
626
|
-
if (depth === 0) return match;
|
|
627
|
-
depth--;
|
|
628
|
-
}
|
|
629
|
-
else if (ch === ',' && depth === 0) return match;
|
|
630
|
-
else if (ch === ';' && depth === 0) break;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return `${propVar}.get()`;
|
|
647
|
+
new RegExp(`(?:(?<=\\.\\.\\.)|(?<!\\.))\\b(${propVarPattern})\\b(?!\\s*=(?!=)|\\s*\\.(?:get|set))`, 'g'),
|
|
648
|
+
(match, varName, offset) => {
|
|
649
|
+
if (isObjectKey(code, match, offset)) return match;
|
|
650
|
+
return `${varName}.get()`;
|
|
635
651
|
}
|
|
636
652
|
);
|
|
637
653
|
}
|
|
@@ -144,10 +144,8 @@ export class Transformer {
|
|
|
144
144
|
// Check directives for a11y usage
|
|
145
145
|
if (node.directives) {
|
|
146
146
|
for (const directive of node.directives) {
|
|
147
|
-
if (directive.type === '
|
|
148
|
-
|
|
149
|
-
this.usesA11y.srOnly = true;
|
|
150
|
-
}
|
|
147
|
+
if (directive.type === 'SrOnlyDirective') {
|
|
148
|
+
this.usesA11y.srOnly = true;
|
|
151
149
|
} else if (directive.type === 'FocusTrapDirective') {
|
|
152
150
|
this.usesA11y.trapFocus = true;
|
|
153
151
|
}
|
|
@@ -113,7 +113,7 @@ function isConditionalGroupAtRule(selector) {
|
|
|
113
113
|
*/
|
|
114
114
|
function isKeyframeStep(selector) {
|
|
115
115
|
const trimmed = selector.trim();
|
|
116
|
-
return trimmed === 'from' || trimmed === 'to' || /^\d
|
|
116
|
+
return trimmed === 'from' || trimmed === 'to' || /^\d+(\.\d+)?%$/.test(trimmed);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -288,6 +288,70 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
|
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Split a CSS selector into tokens, preserving combinators (>, +, ~) as separate tokens.
|
|
293
|
+
* Uses a linear scan instead of a regex split to avoid ReDoS on inputs with many spaces.
|
|
294
|
+
* @param {string} selector - A single CSS selector (no commas)
|
|
295
|
+
* @returns {string[]} Array of tokens (selectors and combinators)
|
|
296
|
+
*/
|
|
297
|
+
function splitSelectorTokens(selector) {
|
|
298
|
+
const tokens = [];
|
|
299
|
+
let current = '';
|
|
300
|
+
let i = 0;
|
|
301
|
+
|
|
302
|
+
while (i < selector.length) {
|
|
303
|
+
const ch = selector[i];
|
|
304
|
+
|
|
305
|
+
// Check for combinator characters
|
|
306
|
+
if (ch === '>' || ch === '+' || ch === '~') {
|
|
307
|
+
// Push any accumulated selector token
|
|
308
|
+
if (current.trim()) {
|
|
309
|
+
tokens.push(current.trim());
|
|
310
|
+
current = '';
|
|
311
|
+
}
|
|
312
|
+
// Push the combinator (with surrounding spaces for formatting)
|
|
313
|
+
tokens.push(` ${ch} `);
|
|
314
|
+
i++;
|
|
315
|
+
// Skip trailing whitespace after combinator
|
|
316
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check for whitespace (descendant combinator)
|
|
321
|
+
if (/\s/.test(ch)) {
|
|
322
|
+
// Consume all consecutive whitespace
|
|
323
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
324
|
+
// Check if whitespace is followed by a combinator
|
|
325
|
+
if (i < selector.length && (selector[i] === '>' || selector[i] === '+' || selector[i] === '~')) {
|
|
326
|
+
// Don't push a space token; the combinator loop iteration will handle it
|
|
327
|
+
if (current.trim()) {
|
|
328
|
+
tokens.push(current.trim());
|
|
329
|
+
current = '';
|
|
330
|
+
}
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Plain whitespace = descendant combinator (space between selectors)
|
|
334
|
+
if (current.trim()) {
|
|
335
|
+
tokens.push(current.trim());
|
|
336
|
+
current = '';
|
|
337
|
+
}
|
|
338
|
+
// Push an empty string that will become a space separator
|
|
339
|
+
tokens.push(' ');
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
current += ch;
|
|
344
|
+
i++;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Push final token
|
|
348
|
+
if (current.trim()) {
|
|
349
|
+
tokens.push(current.trim());
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return tokens.filter(t => t !== '');
|
|
353
|
+
}
|
|
354
|
+
|
|
291
355
|
/**
|
|
292
356
|
* Add scope to CSS selector
|
|
293
357
|
* .container -> .container.p123abc
|
|
@@ -327,22 +391,25 @@ export function scopeStyleSelector(transformer, selector) {
|
|
|
327
391
|
return selector.split(',').map(part => {
|
|
328
392
|
part = part.trim();
|
|
329
393
|
|
|
330
|
-
// Split
|
|
331
|
-
//
|
|
332
|
-
const tokens = part
|
|
394
|
+
// Split selector into tokens, preserving CSS combinators (>, +, ~).
|
|
395
|
+
// Uses a programmatic approach instead of a single regex to avoid ReDoS.
|
|
396
|
+
const tokens = splitSelectorTokens(part);
|
|
333
397
|
const result = [];
|
|
334
398
|
|
|
335
399
|
for (let i = 0; i < tokens.length; i++) {
|
|
336
400
|
const token = tokens[i].trim();
|
|
337
401
|
|
|
338
|
-
// Check if this is a combinator
|
|
402
|
+
// Check if this is a combinator (>, +, ~)
|
|
339
403
|
if (combinators.has(token)) {
|
|
340
404
|
result.push(` ${token} `);
|
|
341
405
|
continue;
|
|
342
406
|
}
|
|
343
407
|
|
|
344
|
-
//
|
|
345
|
-
if (!token)
|
|
408
|
+
// Whitespace-only token = descendant combinator (space between selectors)
|
|
409
|
+
if (!token) {
|
|
410
|
+
result.push(' ');
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
346
413
|
|
|
347
414
|
// Check if this segment is a global selector
|
|
348
415
|
const segmentBase = token.split(/[.#\[]/)[0];
|