pulse-js-framework 1.7.33 → 1.7.38
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/utils/file-utils.js +6 -0
- package/compiler/parser.js +3 -1
- package/compiler/transformer/constants.js +1 -1
- package/compiler/transformer/expressions.js +43 -3
- package/loader/vite-plugin.js +1 -1
- package/package.json +7 -1
- package/runtime/a11y.js +50 -34
- package/runtime/async.js +16 -2
- package/runtime/form.js +35 -4
- package/runtime/router.js +10 -3
- package/runtime/security.js +21 -0
- package/runtime/ssr.js +17 -2
- package/types/a11y.d.ts +186 -0
- package/types/devtools.d.ts +418 -0
- package/types/dom-adapter.d.ts +643 -0
- package/types/dom.d.ts +63 -0
- package/types/errors.d.ts +618 -0
- package/types/http.d.ts +426 -0
- package/types/logger.d.ts +12 -0
- package/types/native.d.ts +282 -0
- package/types/pulse.d.ts +70 -1
- package/types/security.d.ts +286 -0
- package/types/ssr.d.ts +263 -0
- package/types/utils.d.ts +85 -0
package/cli/utils/file-utils.js
CHANGED
|
@@ -80,6 +80,9 @@ function globMatch(base, pattern, extensions) {
|
|
|
80
80
|
match(dir, partIndex + 1);
|
|
81
81
|
try {
|
|
82
82
|
for (const entry of readdirSync(dir)) {
|
|
83
|
+
// Skip hidden files and node_modules
|
|
84
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
|
85
|
+
|
|
83
86
|
const full = join(dir, entry);
|
|
84
87
|
try {
|
|
85
88
|
if (statSync(full).isDirectory()) {
|
|
@@ -153,6 +156,9 @@ function matchFilesInDirRecursive(dir, pattern, extensions, results) {
|
|
|
153
156
|
function walk(currentDir) {
|
|
154
157
|
try {
|
|
155
158
|
for (const entry of readdirSync(currentDir)) {
|
|
159
|
+
// Skip hidden files and node_modules
|
|
160
|
+
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
|
161
|
+
|
|
156
162
|
const full = join(currentDir, entry);
|
|
157
163
|
try {
|
|
158
164
|
const stat = statSync(full);
|
package/compiler/parser.js
CHANGED
|
@@ -100,7 +100,9 @@ export class Parser {
|
|
|
100
100
|
* Peek at token at offset
|
|
101
101
|
*/
|
|
102
102
|
peek(offset = 1) {
|
|
103
|
-
|
|
103
|
+
const index = this.pos + offset;
|
|
104
|
+
if (index < 0 || index >= this.tokens.length) return undefined;
|
|
105
|
+
return this.tokens[index];
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Generate a unique scope ID for CSS scoping */
|
|
8
|
-
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2,
|
|
8
|
+
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 12);
|
|
9
9
|
|
|
10
10
|
/** Token types that should not have space after them */
|
|
11
11
|
export const NO_SPACE_AFTER = new Set([
|
|
@@ -182,14 +182,54 @@ export function transformExpression(transformer, node) {
|
|
|
182
182
|
* @returns {string} Transformed expression string
|
|
183
183
|
*/
|
|
184
184
|
export function transformExpressionString(transformer, exprStr) {
|
|
185
|
-
//
|
|
185
|
+
// Transform state and prop vars in expression strings (interpolations, attribute bindings)
|
|
186
186
|
// Both are now reactive (useProp returns computed for uniform interface)
|
|
187
187
|
let result = exprStr;
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// First, handle assignments to state vars: stateVar = expr -> stateVar.set(expr)
|
|
190
|
+
// This must happen before the generic .get() replacement to avoid generating
|
|
191
|
+
// invalid code like stateVar.get() = expr (LHS of assignment is not a reference)
|
|
190
192
|
for (const stateVar of transformer.stateVars) {
|
|
193
|
+
// Compound assignment: stateVar += expr -> stateVar.update(_v => _v + expr)
|
|
191
194
|
result = result.replace(
|
|
192
|
-
new RegExp(`\\b${stateVar}\\
|
|
195
|
+
new RegExp(`\\b${stateVar}\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g'),
|
|
196
|
+
(_match, op) => {
|
|
197
|
+
const baseOp = op.slice(0, -1); // Remove trailing '='
|
|
198
|
+
return `${stateVar}.update(_v => _v ${baseOp} `;
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
// Close the .update() call - find the end of the expression after the replacement
|
|
202
|
+
// This is handled by the fact that the expression continues after the replacement text
|
|
203
|
+
// and the closing paren is added by wrapping logic below.
|
|
204
|
+
|
|
205
|
+
// Simple assignment: stateVar = expr -> stateVar.set(expr)
|
|
206
|
+
// Use negative lookbehind to skip compound assignments (already handled)
|
|
207
|
+
// Use negative lookahead to skip == and ===
|
|
208
|
+
result = result.replace(
|
|
209
|
+
new RegExp(`\\b${stateVar}\\s*=(?!=)`, 'g'),
|
|
210
|
+
`${stateVar}.set(`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If we inserted .set( or .update(, we need to close the parenthesis
|
|
215
|
+
// Find unclosed .set( and .update( calls and close them at end of expression
|
|
216
|
+
if (result.includes('.set(') || result.includes('.update(_v =>')) {
|
|
217
|
+
// For .update(_v => _v op expr), close with )
|
|
218
|
+
result = result.replace(
|
|
219
|
+
/\.update\(_v => _v [^\)]*$/,
|
|
220
|
+
(m) => m + ')'
|
|
221
|
+
);
|
|
222
|
+
// For .set(expr), close with )
|
|
223
|
+
result = result.replace(
|
|
224
|
+
/\.set\(([^)]*$)/,
|
|
225
|
+
(_m, expr) => `.set(${expr})`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Transform state var reads (not already transformed to .get/.set/.update)
|
|
230
|
+
for (const stateVar of transformer.stateVars) {
|
|
231
|
+
result = result.replace(
|
|
232
|
+
new RegExp(`\\b${stateVar}\\b(?!\\.(?:get|set|update))`, 'g'),
|
|
193
233
|
`${stateVar}.get()`
|
|
194
234
|
);
|
|
195
235
|
}
|
package/loader/vite-plugin.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.38",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "index.js",
|
|
7
8
|
"types": "types/index.d.ts",
|
|
8
9
|
"bin": {
|
|
@@ -178,6 +179,11 @@
|
|
|
178
179
|
"test:esbuild-plugin": "node test/esbuild-plugin.test.js",
|
|
179
180
|
"test:parcel-plugin": "node test/parcel-plugin.test.js",
|
|
180
181
|
"test:swc-plugin": "node test/swc-plugin.test.js",
|
|
182
|
+
"test:dom-binding": "node test/dom-binding.test.js",
|
|
183
|
+
"test:interceptor-manager": "node test/interceptor-manager.test.js",
|
|
184
|
+
"test:vite-plugin": "node --test test/vite-plugin.test.js",
|
|
185
|
+
"test:memory-cleanup": "node --test test/memory-cleanup.test.js",
|
|
186
|
+
"test:dev-server": "node --test test/dev-server.test.js",
|
|
181
187
|
"build:netlify": "node scripts/build-netlify.js",
|
|
182
188
|
"version": "node scripts/sync-version.js",
|
|
183
189
|
"docs": "node cli/index.js dev docs"
|
package/runtime/a11y.js
CHANGED
|
@@ -523,44 +523,39 @@ export function createPreferences() {
|
|
|
523
523
|
const forcedColors = pulse(forcedColorsMode());
|
|
524
524
|
const contrast = pulse(prefersContrast());
|
|
525
525
|
|
|
526
|
-
|
|
527
|
-
// Listen for preference changes
|
|
528
|
-
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', (e) => {
|
|
529
|
-
reducedMotion.set(e.matches);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
533
|
-
colorScheme.set(e.matches ? 'dark' : 'light');
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
window.matchMedia('(prefers-contrast: more)').addEventListener('change', (e) => {
|
|
537
|
-
highContrast.set(e.matches);
|
|
538
|
-
});
|
|
526
|
+
const listeners = [];
|
|
539
527
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
});
|
|
528
|
+
if (typeof window !== 'undefined') {
|
|
529
|
+
const track = (query, handler) => {
|
|
530
|
+
const mql = window.matchMedia(query);
|
|
531
|
+
mql.addEventListener('change', handler);
|
|
532
|
+
listeners.push({ mql, handler });
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
track('(prefers-reduced-motion: reduce)', (e) => reducedMotion.set(e.matches));
|
|
536
|
+
track('(prefers-color-scheme: dark)', (e) => colorScheme.set(e.matches ? 'dark' : 'light'));
|
|
537
|
+
track('(prefers-contrast: more)', (e) => highContrast.set(e.matches));
|
|
538
|
+
track('(prefers-reduced-transparency: reduce)', (e) => reducedTransparency.set(e.matches));
|
|
539
|
+
track('(forced-colors: active)', (e) => forcedColors.set(e.matches ? 'active' : 'none'));
|
|
540
|
+
track('(prefers-contrast: more)', () => contrast.set(prefersContrast()));
|
|
541
|
+
track('(prefers-contrast: less)', () => contrast.set(prefersContrast()));
|
|
555
542
|
}
|
|
556
543
|
|
|
544
|
+
const cleanup = () => {
|
|
545
|
+
for (const { mql, handler } of listeners) {
|
|
546
|
+
mql.removeEventListener('change', handler);
|
|
547
|
+
}
|
|
548
|
+
listeners.length = 0;
|
|
549
|
+
};
|
|
550
|
+
|
|
557
551
|
return {
|
|
558
552
|
reducedMotion,
|
|
559
553
|
colorScheme,
|
|
560
554
|
highContrast,
|
|
561
555
|
reducedTransparency,
|
|
562
556
|
forcedColors,
|
|
563
|
-
contrast
|
|
557
|
+
contrast,
|
|
558
|
+
cleanup
|
|
564
559
|
};
|
|
565
560
|
}
|
|
566
561
|
|
|
@@ -1570,27 +1565,43 @@ export function createAnnouncementQueue(options = {}) {
|
|
|
1570
1565
|
|
|
1571
1566
|
const queue = [];
|
|
1572
1567
|
let isProcessing = false;
|
|
1568
|
+
let currentTimerId = null;
|
|
1569
|
+
let aborted = false;
|
|
1573
1570
|
const queueLength = pulse(0);
|
|
1574
1571
|
|
|
1575
1572
|
const processQueue = async () => {
|
|
1576
|
-
if (isProcessing || queue.length === 0) return;
|
|
1573
|
+
if (isProcessing || queue.length === 0 || aborted) return;
|
|
1577
1574
|
|
|
1578
1575
|
isProcessing = true;
|
|
1579
1576
|
|
|
1580
|
-
while (queue.length > 0) {
|
|
1577
|
+
while (queue.length > 0 && !aborted) {
|
|
1581
1578
|
const { message, priority, clearAfter } = queue.shift();
|
|
1582
1579
|
queueLength.set(queue.length);
|
|
1583
1580
|
|
|
1584
1581
|
announce(message, { priority, clearAfter });
|
|
1585
1582
|
|
|
1586
1583
|
// Wait for announcement to be read
|
|
1587
|
-
await new Promise(resolve =>
|
|
1588
|
-
|
|
1584
|
+
await new Promise(resolve => {
|
|
1585
|
+
currentTimerId = setTimeout(resolve,
|
|
1586
|
+
Math.max(minDelay, clearAfter || 1000));
|
|
1587
|
+
});
|
|
1588
|
+
currentTimerId = null;
|
|
1589
1589
|
}
|
|
1590
1590
|
|
|
1591
1591
|
isProcessing = false;
|
|
1592
1592
|
};
|
|
1593
1593
|
|
|
1594
|
+
const dispose = () => {
|
|
1595
|
+
aborted = true;
|
|
1596
|
+
if (currentTimerId !== null) {
|
|
1597
|
+
clearTimeout(currentTimerId);
|
|
1598
|
+
currentTimerId = null;
|
|
1599
|
+
}
|
|
1600
|
+
queue.length = 0;
|
|
1601
|
+
queueLength.set(0);
|
|
1602
|
+
isProcessing = false;
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1594
1605
|
return {
|
|
1595
1606
|
queueLength,
|
|
1596
1607
|
/**
|
|
@@ -1599,6 +1610,7 @@ export function createAnnouncementQueue(options = {}) {
|
|
|
1599
1610
|
* @param {object} options - Announcement options (priority, clearAfter)
|
|
1600
1611
|
*/
|
|
1601
1612
|
add: (message, opts = {}) => {
|
|
1613
|
+
if (aborted) return;
|
|
1602
1614
|
queue.push({ message, ...opts });
|
|
1603
1615
|
queueLength.set(queue.length);
|
|
1604
1616
|
processQueue();
|
|
@@ -1614,7 +1626,11 @@ export function createAnnouncementQueue(options = {}) {
|
|
|
1614
1626
|
* Check if queue is being processed
|
|
1615
1627
|
* @returns {boolean}
|
|
1616
1628
|
*/
|
|
1617
|
-
isProcessing: () => isProcessing
|
|
1629
|
+
isProcessing: () => isProcessing,
|
|
1630
|
+
/**
|
|
1631
|
+
* Dispose the queue, cancelling any pending timers
|
|
1632
|
+
*/
|
|
1633
|
+
dispose
|
|
1618
1634
|
};
|
|
1619
1635
|
}
|
|
1620
1636
|
|
package/runtime/async.js
CHANGED
|
@@ -462,6 +462,11 @@ export function useAsync(asyncFn, options = {}) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
const dispose = () => {
|
|
466
|
+
versionController.cleanup();
|
|
467
|
+
};
|
|
468
|
+
onCleanup(dispose);
|
|
469
|
+
|
|
465
470
|
// Execute immediately if requested
|
|
466
471
|
if (immediate) {
|
|
467
472
|
execute();
|
|
@@ -474,7 +479,8 @@ export function useAsync(asyncFn, options = {}) {
|
|
|
474
479
|
status,
|
|
475
480
|
execute,
|
|
476
481
|
reset,
|
|
477
|
-
abort
|
|
482
|
+
abort,
|
|
483
|
+
dispose
|
|
478
484
|
};
|
|
479
485
|
}
|
|
480
486
|
|
|
@@ -727,6 +733,13 @@ export function useResource(key, fetcher, options = {}) {
|
|
|
727
733
|
fetch();
|
|
728
734
|
}
|
|
729
735
|
|
|
736
|
+
const dispose = () => {
|
|
737
|
+
if (intervalId) {
|
|
738
|
+
clearInterval(intervalId);
|
|
739
|
+
intervalId = null;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
730
743
|
return {
|
|
731
744
|
data,
|
|
732
745
|
error,
|
|
@@ -737,7 +750,8 @@ export function useResource(key, fetcher, options = {}) {
|
|
|
737
750
|
fetch,
|
|
738
751
|
refresh,
|
|
739
752
|
mutate,
|
|
740
|
-
invalidate
|
|
753
|
+
invalidate,
|
|
754
|
+
dispose
|
|
741
755
|
};
|
|
742
756
|
}
|
|
743
757
|
|
package/runtime/form.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and touched state tracking.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { pulse, effect, computed, batch } from './pulse.js';
|
|
9
|
+
import { pulse, effect, computed, batch, onCleanup } from './pulse.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {Object} FieldState
|
|
@@ -698,6 +698,19 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
const dispose = () => {
|
|
702
|
+
for (const name of fieldNames) {
|
|
703
|
+
const timers = debounceTimers[name];
|
|
704
|
+
if (timers) {
|
|
705
|
+
for (const timerId of timers.values()) {
|
|
706
|
+
clearTimeout(timerId);
|
|
707
|
+
}
|
|
708
|
+
timers.clear();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
onCleanup(dispose);
|
|
713
|
+
|
|
701
714
|
return {
|
|
702
715
|
fields,
|
|
703
716
|
isValid,
|
|
@@ -715,7 +728,8 @@ export function useForm(initialValues, validationSchema = {}, options = {}) {
|
|
|
715
728
|
reset,
|
|
716
729
|
handleSubmit,
|
|
717
730
|
setErrors,
|
|
718
|
-
clearErrors
|
|
731
|
+
clearErrors,
|
|
732
|
+
dispose
|
|
719
733
|
};
|
|
720
734
|
}
|
|
721
735
|
|
|
@@ -905,6 +919,14 @@ export function useField(initialValue, rules = [], options = {}) {
|
|
|
905
919
|
});
|
|
906
920
|
};
|
|
907
921
|
|
|
922
|
+
const dispose = () => {
|
|
923
|
+
for (const timerId of debounceTimers.values()) {
|
|
924
|
+
clearTimeout(timerId);
|
|
925
|
+
}
|
|
926
|
+
debounceTimers.clear();
|
|
927
|
+
};
|
|
928
|
+
onCleanup(dispose);
|
|
929
|
+
|
|
908
930
|
return {
|
|
909
931
|
value,
|
|
910
932
|
error,
|
|
@@ -918,7 +940,8 @@ export function useField(initialValue, rules = [], options = {}) {
|
|
|
918
940
|
onBlur,
|
|
919
941
|
reset,
|
|
920
942
|
setError: (msg) => error.set(msg),
|
|
921
|
-
clearError: () => error.set(null)
|
|
943
|
+
clearError: () => error.set(null),
|
|
944
|
+
dispose
|
|
922
945
|
};
|
|
923
946
|
}
|
|
924
947
|
|
|
@@ -1028,6 +1051,13 @@ export function useFieldArray(initialValues = [], itemRules = []) {
|
|
|
1028
1051
|
return asyncResults.every(r => r === true);
|
|
1029
1052
|
};
|
|
1030
1053
|
|
|
1054
|
+
const dispose = () => {
|
|
1055
|
+
for (const field of fieldsArray.get()) {
|
|
1056
|
+
field.dispose?.();
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
onCleanup(dispose);
|
|
1060
|
+
|
|
1031
1061
|
return {
|
|
1032
1062
|
fields: fieldsArray,
|
|
1033
1063
|
values,
|
|
@@ -1042,7 +1072,8 @@ export function useFieldArray(initialValues = [], itemRules = []) {
|
|
|
1042
1072
|
replace,
|
|
1043
1073
|
reset,
|
|
1044
1074
|
validateAll,
|
|
1045
|
-
validateAllSync
|
|
1075
|
+
validateAllSync,
|
|
1076
|
+
dispose
|
|
1046
1077
|
};
|
|
1047
1078
|
}
|
|
1048
1079
|
|
package/runtime/router.js
CHANGED
|
@@ -878,16 +878,18 @@ export function createRouter(options = {}) {
|
|
|
878
878
|
const a = el('a', content);
|
|
879
879
|
a.href = href;
|
|
880
880
|
|
|
881
|
-
|
|
881
|
+
const handleClick = (e) => {
|
|
882
882
|
// Allow ctrl/cmd+click for new tab
|
|
883
883
|
if (e.ctrlKey || e.metaKey) return;
|
|
884
884
|
|
|
885
885
|
e.preventDefault();
|
|
886
886
|
navigate(path, options);
|
|
887
|
-
}
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
a.addEventListener('click', handleClick);
|
|
888
890
|
|
|
889
891
|
// Add active class when route matches
|
|
890
|
-
effect(() => {
|
|
892
|
+
const disposeEffect = effect(() => {
|
|
891
893
|
const current = currentPath.get();
|
|
892
894
|
if (current === path || (options.exact === false && current.startsWith(path))) {
|
|
893
895
|
a.classList.add(options.activeClass || 'active');
|
|
@@ -896,6 +898,11 @@ export function createRouter(options = {}) {
|
|
|
896
898
|
}
|
|
897
899
|
});
|
|
898
900
|
|
|
901
|
+
a.cleanup = () => {
|
|
902
|
+
a.removeEventListener('click', handleClick);
|
|
903
|
+
disposeEffect();
|
|
904
|
+
};
|
|
905
|
+
|
|
899
906
|
return a;
|
|
900
907
|
}
|
|
901
908
|
|
package/runtime/security.js
CHANGED
|
@@ -321,6 +321,27 @@ export function sanitizeHtml(html, options = {}) {
|
|
|
321
321
|
attrValue = sanitized;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// Sanitize style attribute to prevent CSS injection
|
|
325
|
+
if (attrName === 'style') {
|
|
326
|
+
const parts = attrValue.split(';').filter(Boolean);
|
|
327
|
+
const safeParts = [];
|
|
328
|
+
for (const part of parts) {
|
|
329
|
+
const colonIndex = part.indexOf(':');
|
|
330
|
+
if (colonIndex === -1) continue;
|
|
331
|
+
const cssProp = part.slice(0, colonIndex).trim();
|
|
332
|
+
const cssVal = part.slice(colonIndex + 1).trim();
|
|
333
|
+
if (/url\s*\(/i.test(cssVal) || /expression\s*\(/i.test(cssVal) ||
|
|
334
|
+
/javascript:/i.test(cssVal) || /behavior\s*:/i.test(cssVal) ||
|
|
335
|
+
/-moz-binding/i.test(cssVal)) {
|
|
336
|
+
log.warn(`Blocked dangerous CSS in style attribute: ${cssProp}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
safeParts.push(`${cssProp}: ${cssVal}`);
|
|
340
|
+
}
|
|
341
|
+
attrValue = safeParts.join('; ');
|
|
342
|
+
if (!attrValue) continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
result += ` ${attrName}="${escapeHtml(attrValue)}"`;
|
|
325
346
|
}
|
|
326
347
|
|
package/runtime/ssr.js
CHANGED
|
@@ -408,12 +408,16 @@ export function deserializeState(data) {
|
|
|
408
408
|
* // Default implementation just stores in global
|
|
409
409
|
* restoreState(window.__PULSE_STATE__);
|
|
410
410
|
*/
|
|
411
|
+
// Module-scoped state store (avoids globalThis collision between multiple SSR instances)
|
|
412
|
+
let _ssrState = null;
|
|
413
|
+
|
|
411
414
|
export function restoreState(state) {
|
|
412
415
|
const deserialized = typeof state === 'string'
|
|
413
416
|
? deserializeState(state)
|
|
414
417
|
: state;
|
|
415
418
|
|
|
416
|
-
// Store in global for
|
|
419
|
+
// Store in module scope and global for backward compatibility
|
|
420
|
+
_ssrState = deserialized;
|
|
417
421
|
if (typeof globalThis !== 'undefined') {
|
|
418
422
|
globalThis.__PULSE_SSR_STATE__ = deserialized;
|
|
419
423
|
}
|
|
@@ -430,10 +434,20 @@ export function restoreState(state) {
|
|
|
430
434
|
* const userData = getSSRState('user');
|
|
431
435
|
*/
|
|
432
436
|
export function getSSRState(key) {
|
|
433
|
-
const state = globalThis?.__PULSE_SSR_STATE__ || {};
|
|
437
|
+
const state = _ssrState || globalThis?.__PULSE_SSR_STATE__ || {};
|
|
434
438
|
return key ? state[key] : state;
|
|
435
439
|
}
|
|
436
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Clear the SSR state. Use in tests or when cleaning up SSR context.
|
|
443
|
+
*/
|
|
444
|
+
export function clearSSRState() {
|
|
445
|
+
_ssrState = null;
|
|
446
|
+
if (typeof globalThis !== 'undefined') {
|
|
447
|
+
delete globalThis.__PULSE_SSR_STATE__;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
437
451
|
// ============================================================================
|
|
438
452
|
// Re-exports for convenience
|
|
439
453
|
// ============================================================================
|
|
@@ -456,6 +470,7 @@ export default {
|
|
|
456
470
|
deserializeState,
|
|
457
471
|
restoreState,
|
|
458
472
|
getSSRState,
|
|
473
|
+
clearSSRState,
|
|
459
474
|
|
|
460
475
|
// Mode checks
|
|
461
476
|
isSSR,
|