@akinon/pz-theme 2.0.31-beta.0 → 2.0.31
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/CHANGELOG.md +7 -3
- package/jest.config.js +6 -3
- package/package.json +5 -4
- package/src/__tests__/theme-block.test.tsx +19 -0
- package/src/components/__tests__/section-error-boundary.test.tsx +64 -0
- package/src/components/section-error-boundary.tsx +47 -0
- package/src/schemas/__tests__/theme-schema.test.ts +93 -0
- package/src/schemas/theme-schema.ts +65 -0
- package/src/sections/featured-product-spotlight-section.tsx +2 -1
- package/src/sections/posts-slider-section.tsx +2 -1
- package/src/sections/pre-order-launch-banner-section.tsx +2 -1
- package/src/sections/shipping-threshold-progress-section.tsx +7 -5
- package/src/theme-block.tsx +8 -1
- package/src/theme-placeholder-client.tsx +55 -52
- package/src/theme-placeholder-wrapper.tsx +52 -49
- package/src/theme-placeholder.tsx +29 -0
- package/src/theme-section.tsx +22 -2
- package/src/utils/__tests__/css-safety.test.ts +80 -0
- package/src/utils/__tests__/dev-warn.test.ts +63 -0
- package/src/utils/__tests__/safe-clone.test.ts +42 -0
- package/src/utils/__tests__/style-warn.test.ts +71 -0
- package/src/utils/css-safety.ts +29 -0
- package/src/utils/dev-warn.ts +46 -0
- package/src/utils/index.ts +42 -3
- package/src/utils/iterator-utils.ts +15 -3
- package/src/utils/safe-clone.ts +25 -0
- package/src/utils/visibility-rules.test.ts +36 -0
- package/src/utils/visibility-rules.ts +12 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Block } from '../theme-block';
|
|
2
|
+
import { devWarn } from './dev-warn';
|
|
2
3
|
|
|
3
4
|
interface BindingContext {
|
|
4
5
|
sectionDataSource?: any;
|
|
@@ -460,9 +461,20 @@ const getIteratorItems = ({
|
|
|
460
461
|
|
|
461
462
|
if (dataPath) {
|
|
462
463
|
const resolvedValue = getValueAtPath(bindingRoot, dataPath);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
if (Array.isArray(resolvedValue)) {
|
|
465
|
+
return resolvedValue as Record<string, unknown>[];
|
|
466
|
+
}
|
|
467
|
+
// A defined-but-non-array result means iteratorDataPath points at the wrong
|
|
468
|
+
// shape (e.g. a scalar/object instead of a list). Surface it in dev so
|
|
469
|
+
// authors catch binding typos; an unresolved (undefined) path is left
|
|
470
|
+
// silent because it is ambiguous with data that has not loaded yet.
|
|
471
|
+
if (resolvedValue !== undefined) {
|
|
472
|
+
devWarn(
|
|
473
|
+
'iterator',
|
|
474
|
+
`iteratorDataPath "${dataPath}" resolved to a non-array value; rendering no items`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return [];
|
|
466
478
|
}
|
|
467
479
|
|
|
468
480
|
if (Array.isArray((bindingRoot as any).items)) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-clone that never throws during render.
|
|
3
|
+
*
|
|
4
|
+
* Uses the existing JSON round-trip for the normal (serializable) case so the
|
|
5
|
+
* cloned output is byte-identical to the previous behavior, then falls back to
|
|
6
|
+
* structuredClone (which handles circular references) and finally to the
|
|
7
|
+
* original reference if the value cannot be cloned at all. This is strictly
|
|
8
|
+
* better than a bare JSON.parse(JSON.stringify(...)), which throws on
|
|
9
|
+
* non-serializable values (circular refs, BigInt) and — with no error boundary
|
|
10
|
+
* around it — used to crash the entire placeholder.
|
|
11
|
+
*/
|
|
12
|
+
export const safeDeepClone = <T>(value: T): T => {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(JSON.stringify(value));
|
|
15
|
+
} catch {
|
|
16
|
+
if (typeof structuredClone === 'function') {
|
|
17
|
+
try {
|
|
18
|
+
return structuredClone(value);
|
|
19
|
+
} catch {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
evaluateVisibilityRules,
|
|
3
|
+
normalizeVisibilityRules,
|
|
3
4
|
VisibilityRuleContext
|
|
4
5
|
} from './visibility-rules';
|
|
6
|
+
import { resetDevWarnCacheForTests } from './dev-warn';
|
|
5
7
|
|
|
6
8
|
const makeContext = (
|
|
7
9
|
overrides: Partial<VisibilityRuleContext> = {}
|
|
@@ -153,3 +155,37 @@ describe('evaluateVisibilityRules', () => {
|
|
|
153
155
|
});
|
|
154
156
|
});
|
|
155
157
|
});
|
|
158
|
+
|
|
159
|
+
describe('normalizeVisibilityRules — malformed input', () => {
|
|
160
|
+
let warnSpy: jest.SpyInstance;
|
|
161
|
+
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
resetDevWarnCacheForTests();
|
|
164
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
afterEach(() => {
|
|
168
|
+
warnSpy.mockRestore();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('dev-warns for a truthy-but-non-object value (still fails open)', () => {
|
|
172
|
+
const rules = normalizeVisibilityRules('oops' as unknown);
|
|
173
|
+
expect(rules.enabled).toBe(false);
|
|
174
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
175
|
+
'[pz-theme:visibilityRules] Expected a rules object but received string; ignoring rules (content stays visible)'
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('dev-warns for an array value (reports "an array")', () => {
|
|
180
|
+
normalizeVisibilityRules([1, 2, 3] as unknown);
|
|
181
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
182
|
+
'[pz-theme:visibilityRules] Expected a rules object but received an array; ignoring rules (content stays visible)'
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('stays silent for the legitimate "no rules" case (undefined / null)', () => {
|
|
187
|
+
normalizeVisibilityRules(undefined);
|
|
188
|
+
normalizeVisibilityRules(null as unknown);
|
|
189
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Block } from '../theme-block';
|
|
2
2
|
import { Section } from '../theme-section';
|
|
3
|
+
import { devWarn } from './dev-warn';
|
|
3
4
|
|
|
4
5
|
export type VisibilityAuthState = 'all' | 'authenticated' | 'guest';
|
|
5
6
|
export type VisibilityPathMatchType =
|
|
@@ -40,6 +41,17 @@ const normalizeStringArray = (value: unknown): string[] => {
|
|
|
40
41
|
|
|
41
42
|
export const normalizeVisibilityRules = (value: unknown): VisibilityRules => {
|
|
42
43
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
44
|
+
// A truthy-but-non-object value (string, number, array) is a malformed
|
|
45
|
+
// rules config; it fails open (content stays visible). undefined/null is
|
|
46
|
+
// the legitimate "no rules" case and stays silent.
|
|
47
|
+
if (value) {
|
|
48
|
+
devWarn(
|
|
49
|
+
'visibilityRules',
|
|
50
|
+
`Expected a rules object but received ${
|
|
51
|
+
Array.isArray(value) ? 'an array' : typeof value
|
|
52
|
+
}; ignoring rules (content stays visible)`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
43
55
|
return {
|
|
44
56
|
enabled: false,
|
|
45
57
|
authState: 'all',
|