@akinon/pz-theme 2.0.65-rc.0 → 2.0.65
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 -31
- package/package.json +4 -4
- package/src/blocks/order-detail-block.tsx +600 -124
- package/src/blocks/order-list-block.tsx +477 -101
- package/src/overlays/__tests__/server-read-path-client-free.test.ts +119 -0
- package/src/overlays/modal-registry.ts +4 -1
- package/src/overlays/overlay-surface.tsx +17 -16
- package/src/overlays/overlay-tokens.ts +34 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The server-side theme-config read path must not reach a `'use client'`
|
|
6
|
+
* module.
|
|
7
|
+
*
|
|
8
|
+
* A Server Component importing a VALUE from a client module does not get the
|
|
9
|
+
* value: React hands back a client reference. `OVERLAY_WIDTH_UNITS` arrived as
|
|
10
|
+
* a function, `allowed.includes(...)` threw inside the registry parser, and the
|
|
11
|
+
* reader's catch turned that into an entirely empty theme config on the
|
|
12
|
+
* storefront — no modals, no theme settings, no data sources — with nothing
|
|
13
|
+
* logged. A composed modal simply never opened, and the trigger looked broken.
|
|
14
|
+
*
|
|
15
|
+
* No runtime test can catch this: Jest resolves a `'use client'` module like
|
|
16
|
+
* any other, so the constants are real arrays under test and every assertion
|
|
17
|
+
* passes while the storefront is broken. The import graph is the only place
|
|
18
|
+
* the mistake is visible, so this walks it from source.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const SRC_ROOT = path.resolve(__dirname, '../..');
|
|
22
|
+
|
|
23
|
+
/** Entry points that are read during a server render. */
|
|
24
|
+
const SERVER_ENTRY_POINTS = [
|
|
25
|
+
'chrome/theme-config-source.ts',
|
|
26
|
+
'overlays/modal-registry.ts'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const CANDIDATE_EXTENSIONS = ['.ts', '.tsx', '/index.ts', '/index.tsx'];
|
|
30
|
+
|
|
31
|
+
const resolveImport = (fromFile: string, specifier: string): string | null => {
|
|
32
|
+
// Only relative imports are ours to police; a bare specifier is a package,
|
|
33
|
+
// and a package's own client boundaries are its business.
|
|
34
|
+
if (!specifier.startsWith('.')) return null;
|
|
35
|
+
|
|
36
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
37
|
+
|
|
38
|
+
for (const extension of CANDIDATE_EXTENSIONS) {
|
|
39
|
+
const candidate = `${base}${extension}`;
|
|
40
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const IMPORT_PATTERN = /(?:from|import)\s*['"]([^'"]+)['"]/g;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Type-only imports are erased at compile time, so they cannot carry a client
|
|
52
|
+
* reference and a server module may freely import a type from a client module.
|
|
53
|
+
*/
|
|
54
|
+
const isTypeOnlyImport = (source: string, index: number): boolean => {
|
|
55
|
+
const statementStart = source.lastIndexOf('import', index);
|
|
56
|
+
if (statementStart === -1) return false;
|
|
57
|
+
|
|
58
|
+
return /^import\s+type\b/.test(source.slice(statementStart, index));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isClientModule = (source: string): boolean =>
|
|
62
|
+
/^\s*(['"])use client\1/.test(source);
|
|
63
|
+
|
|
64
|
+
const collectReachableModules = (entryPoints: string[]): Map<string, string> => {
|
|
65
|
+
const visited = new Map<string, string>();
|
|
66
|
+
const queue = entryPoints.map((entry) => path.join(SRC_ROOT, entry));
|
|
67
|
+
|
|
68
|
+
while (queue.length > 0) {
|
|
69
|
+
const file = queue.shift() as string;
|
|
70
|
+
if (visited.has(file)) continue;
|
|
71
|
+
|
|
72
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
73
|
+
visited.set(file, source);
|
|
74
|
+
|
|
75
|
+
for (const match of source.matchAll(IMPORT_PATTERN)) {
|
|
76
|
+
if (isTypeOnlyImport(source, match.index ?? 0)) continue;
|
|
77
|
+
|
|
78
|
+
const resolved = resolveImport(file, match[1]);
|
|
79
|
+
if (resolved) queue.push(resolved);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return visited;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe('server-side theme-config read path', () => {
|
|
87
|
+
it('reaches no "use client" module', () => {
|
|
88
|
+
const reachable = collectReachableModules(SERVER_ENTRY_POINTS);
|
|
89
|
+
|
|
90
|
+
const clientModules = Array.from(reachable.entries())
|
|
91
|
+
.filter(([, source]) => isClientModule(source))
|
|
92
|
+
.map(([file]) => path.relative(SRC_ROOT, file));
|
|
93
|
+
|
|
94
|
+
expect(clientModules).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('sees the overlay tokens as real values, not client references', () => {
|
|
98
|
+
// Guards the specific shape of the original break: the parser calls
|
|
99
|
+
// `.includes` on this array and `.slice` on the colour.
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
101
|
+
const tokens = require('../overlay-tokens');
|
|
102
|
+
|
|
103
|
+
expect(Array.isArray(tokens.OVERLAY_WIDTH_UNITS)).toBe(true);
|
|
104
|
+
expect(typeof tokens.OVERLAY_BACKDROP_COLOR).toBe('string');
|
|
105
|
+
expect(typeof tokens.OVERLAY_BACKDROP_OPACITY).toBe('number');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('keeps the tokens importable from the client surface too', () => {
|
|
109
|
+
// The re-export is what stops this fix from breaking every existing
|
|
110
|
+
// `from './overlay-surface'` import.
|
|
111
|
+
const surfaceSource = fs.readFileSync(
|
|
112
|
+
path.join(SRC_ROOT, 'overlays/overlay-surface.tsx'),
|
|
113
|
+
'utf8'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(surfaceSource).toMatch(/export\s*{[^}]*OVERLAY_WIDTH_UNITS/);
|
|
117
|
+
expect(surfaceSource).toMatch(/from '\.\/overlay-tokens'/);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { modalSlug } from '../chrome/placeholder-slugs';
|
|
2
2
|
import { devWarn } from '../utils/dev-warn';
|
|
3
3
|
import type { VisibilityRules } from '../utils/visibility-rules';
|
|
4
|
+
// From `overlay-tokens`, NOT from `overlay-surface`: this module is parsed on
|
|
5
|
+
// the server, and a value imported from a `'use client'` module arrives as a
|
|
6
|
+
// client reference rather than the value itself.
|
|
4
7
|
import {
|
|
5
8
|
OVERLAY_BACKDROP_COLOR,
|
|
6
9
|
OVERLAY_BACKDROP_OPACITY,
|
|
7
10
|
OVERLAY_WIDTH_UNITS,
|
|
8
11
|
type OverlayWidthUnit
|
|
9
|
-
} from './overlay-
|
|
12
|
+
} from './overlay-tokens';
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* The modal registry: what modals this theme has, read off `theme-config`.
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { partAttrs } from '../utils';
|
|
5
|
+
import {
|
|
6
|
+
OVERLAY_BACKDROP_COLOR,
|
|
7
|
+
OVERLAY_BACKDROP_OPACITY
|
|
8
|
+
} from './overlay-tokens';
|
|
9
|
+
import type { OverlayPresentation, OverlayWidthUnit } from './overlay-tokens';
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
12
|
* The overlay surface: how a panel arrives over the page, and what it looks
|
|
@@ -22,18 +27,18 @@ import { partAttrs } from '../utils';
|
|
|
22
27
|
* `panel-legacy-golden.json` pins byte for byte. New callers pass their own.
|
|
23
28
|
*/
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export
|
|
30
|
+
/**
|
|
31
|
+
* Re-exported from `overlay-tokens.ts`, which owns them so the server-side
|
|
32
|
+
* registry parser can import them without going through this client module.
|
|
33
|
+
* Client callers keep importing from here.
|
|
34
|
+
*/
|
|
35
|
+
export {
|
|
36
|
+
OVERLAY_PRESENTATIONS,
|
|
37
|
+
OVERLAY_WIDTH_UNITS,
|
|
38
|
+
OVERLAY_BACKDROP_COLOR,
|
|
39
|
+
OVERLAY_BACKDROP_OPACITY
|
|
40
|
+
} from './overlay-tokens';
|
|
41
|
+
export type { OverlayPresentation, OverlayWidthUnit } from './overlay-tokens';
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
44
|
* The overlay width as CSS. Unit-less callers (the panel block, whose markup
|
|
@@ -58,10 +63,6 @@ export const clampOverlayNumber = (
|
|
|
58
63
|
return Math.min(max, Math.max(min, numeric));
|
|
59
64
|
};
|
|
60
65
|
|
|
61
|
-
/** Backdrop paint defaults — what every published surface shipped with. */
|
|
62
|
-
export const OVERLAY_BACKDROP_COLOR = '#000000';
|
|
63
|
-
export const OVERLAY_BACKDROP_OPACITY = 50;
|
|
64
|
-
|
|
65
66
|
const HEX_COLOR_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay vocabulary: presentations, width units and backdrop defaults.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately NOT in `overlay-surface.tsx`, which is a `'use client'` module.
|
|
5
|
+
* `modal-registry.ts` normalises the registry on the SERVER — it is what
|
|
6
|
+
* `theme-config-source.ts` reads `theme_config` through — and a server module
|
|
7
|
+
* importing a value from a client module does not receive the value: React
|
|
8
|
+
* hands back a client reference, so `OVERLAY_WIDTH_UNITS` arrives as a
|
|
9
|
+
* function and `allowed.includes(...)` throws. That throw used to surface as
|
|
10
|
+
* an entirely EMPTY theme config on the storefront (no modals, no settings, no
|
|
11
|
+
* data sources), because the registry is parsed first and the reader's catch
|
|
12
|
+
* swallows the reason.
|
|
13
|
+
*
|
|
14
|
+
* So the shared vocabulary lives in a module both sides can import. Anything
|
|
15
|
+
* here must stay free of React and of client-only APIs; `overlay-surface.tsx`
|
|
16
|
+
* re-exports it so client callers keep their existing import path.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type OverlayPresentation = 'modal' | 'drawer' | 'sheet' | 'anchored';
|
|
20
|
+
|
|
21
|
+
export const OVERLAY_PRESENTATIONS: OverlayPresentation[] = [
|
|
22
|
+
'modal',
|
|
23
|
+
'drawer',
|
|
24
|
+
'sheet',
|
|
25
|
+
'anchored'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export type OverlayWidthUnit = 'px' | '%' | 'vw';
|
|
29
|
+
|
|
30
|
+
export const OVERLAY_WIDTH_UNITS: OverlayWidthUnit[] = ['px', '%', 'vw'];
|
|
31
|
+
|
|
32
|
+
/** Backdrop paint defaults — what every published surface shipped with. */
|
|
33
|
+
export const OVERLAY_BACKDROP_COLOR = '#000000';
|
|
34
|
+
export const OVERLAY_BACKDROP_OPACITY = 50;
|