@akinon/pz-theme 2.0.43 → 2.0.44
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
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
+
## 2.0.44
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @akinon/next@2.0.44
|
|
8
|
+
- @akinon/theme-contract@2.0.44
|
|
9
|
+
|
|
3
10
|
## 2.0.43
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
6
13
|
|
|
7
|
-
-
|
|
14
|
+
- 0e384d73c: ZERO-4730: Make the checkout page composable with a checkout pieces bridge, outlet blocks and a body takeover region
|
|
8
15
|
- @akinon/next@2.0.43
|
|
9
16
|
- @akinon/theme-contract@2.0.43
|
|
10
17
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.44",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.
|
|
11
|
+
"@akinon/next": "2.0.44",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.
|
|
16
|
+
"@akinon/theme-contract": "2.0.44",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.
|
|
23
|
+
"@akinon/next": "2.0.44",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The checkout wiring carries invariants that live in the source text rather
|
|
6
|
+
* than in exported code:
|
|
7
|
+
*
|
|
8
|
+
* - the `checkout-page-body` slot must mount BEFORE the flow's subtree, so
|
|
9
|
+
* outlet targets register in their `useLayoutEffect` before the flow's
|
|
10
|
+
* mount latch runs (the teleport decision is made pre-first-paint);
|
|
11
|
+
* - no checkout slot may take `fallback={children}`, or a published
|
|
12
|
+
* composition could REPLACE the payment flow instead of arranging it;
|
|
13
|
+
* - the body's takeover affordance sits on the flow itself, and exactly one
|
|
14
|
+
* thing offers it — the slot's dashed band stands down for the region.
|
|
15
|
+
*
|
|
16
|
+
* They are checked against BOTH checkout layouts in the repo: the reference
|
|
17
|
+
* app and the CLI scaffold every new brand is generated from. The two must
|
|
18
|
+
* agree, because the route tables that decide which placeholder `/orders/
|
|
19
|
+
* checkout` composes (`route-slug.ts` here, `route-placeholder-scope.ts` in
|
|
20
|
+
* the editor) are global: the editor offers the four checkout templates on
|
|
21
|
+
* that route for EVERY brand. A scaffold that mounts no `checkout-page-body`
|
|
22
|
+
* slot would let a designer compose, publish, and watch the storefront render
|
|
23
|
+
* nothing — with the flow still claiming to be code-owned underneath.
|
|
24
|
+
*
|
|
25
|
+
* Like `account-slot-coverage`, this suite fs-reads the sources and skips
|
|
26
|
+
* whichever tree is not checked out next to the package.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const REPO_ROOT = path.resolve(__dirname, '../../../..');
|
|
30
|
+
|
|
31
|
+
const CHECKOUT_DIRS = {
|
|
32
|
+
'the reference app': path.join(
|
|
33
|
+
REPO_ROOT,
|
|
34
|
+
'apps/projectzeronext/src/app/[pz]/orders/checkout'
|
|
35
|
+
),
|
|
36
|
+
'the CLI scaffold': path.join(
|
|
37
|
+
REPO_ROOT,
|
|
38
|
+
'packages/projectzero/app-template/src/app/[pz]/orders/checkout'
|
|
39
|
+
)
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const countMatches = (source: string, pattern: RegExp) =>
|
|
43
|
+
(source.match(pattern) ?? []).length;
|
|
44
|
+
|
|
45
|
+
describe.each(Object.entries(CHECKOUT_DIRS))(
|
|
46
|
+
'checkout slot coverage in %s',
|
|
47
|
+
(_name, checkoutDir) => {
|
|
48
|
+
const maybeIt = fs.existsSync(checkoutDir) ? it : it.skip;
|
|
49
|
+
|
|
50
|
+
const layoutPath = path.join(checkoutDir, 'layout.tsx');
|
|
51
|
+
const pagePath = path.join(checkoutDir, 'page.tsx');
|
|
52
|
+
const readLayout = () => fs.readFileSync(layoutPath, 'utf8');
|
|
53
|
+
|
|
54
|
+
maybeIt(
|
|
55
|
+
'mounts each of the four checkout zones exactly once in the layout',
|
|
56
|
+
() => {
|
|
57
|
+
const layout = readLayout();
|
|
58
|
+
|
|
59
|
+
// Counted per SLOT, not per slug mention: the body slug is named twice on
|
|
60
|
+
// purpose — once by its slot, once by the region that offers its takeover
|
|
61
|
+
// — while "one slot per zone" is the invariant that actually matters.
|
|
62
|
+
expect(countMatches(layout, /<ChromeSlot\b/g)).toBe(3);
|
|
63
|
+
|
|
64
|
+
(['checkoutTop', 'checkoutBody', 'checkoutBottom'] as const).forEach(
|
|
65
|
+
(key) => {
|
|
66
|
+
expect(
|
|
67
|
+
countMatches(
|
|
68
|
+
layout,
|
|
69
|
+
// `[^>]` spans newlines, so a prettier reflow of the props cannot
|
|
70
|
+
// break this.
|
|
71
|
+
new RegExp(
|
|
72
|
+
`<ChromeSlot[^>]*PLACEHOLDER_SLUGS\\.${key}\\b[^>]*>`,
|
|
73
|
+
'g'
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
maybeIt('wraps the checkout subtree in the pieces provider', () => {
|
|
82
|
+
expect(readLayout()).toContain('CheckoutPiecesProvider');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
maybeIt(
|
|
86
|
+
'mounts the body slot before the flow (pre-paint latch ordering)',
|
|
87
|
+
() => {
|
|
88
|
+
const layout = readLayout();
|
|
89
|
+
const bodyMountIndex = layout.indexOf('PLACEHOLDER_SLUGS.checkoutBody');
|
|
90
|
+
const flowIndex = layout.indexOf('<ComposableRegion');
|
|
91
|
+
|
|
92
|
+
expect(bodyMountIndex).toBeGreaterThan(-1);
|
|
93
|
+
expect(flowIndex).toBeGreaterThan(-1);
|
|
94
|
+
// Ordering is load-bearing: outlets must register before the flow's
|
|
95
|
+
// mount latch decides where the flow renders.
|
|
96
|
+
expect(bodyMountIndex).toBeLessThan(flowIndex);
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
maybeIt('never hands children to a slot as fallback', () => {
|
|
101
|
+
// A slot with `fallback={children}` would let a composition replace the
|
|
102
|
+
// payment flow instead of positioning it.
|
|
103
|
+
expect(readLayout()).not.toContain('fallback={children}');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
maybeIt('wires the flow piece in the checkout page', () => {
|
|
107
|
+
// The flow-never-lost guarantee is enforced by the runtime piece wrapper —
|
|
108
|
+
// this pins that the page actually mounts it. Without it every outlet a
|
|
109
|
+
// composition places is a dead target: the editor happily accepts the
|
|
110
|
+
// composition and the flow never moves.
|
|
111
|
+
expect(fs.readFileSync(pagePath, 'utf8')).toContain('piece="flow"');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
maybeIt(
|
|
115
|
+
'gives the checkout body a takeover affordance instead of an empty-zone band',
|
|
116
|
+
() => {
|
|
117
|
+
// The point of the region: the designer hovers the REAL checkout content
|
|
118
|
+
// and gets the same "Edit in theme editor" affordance every other page
|
|
119
|
+
// has, rather than a dashed band floating above content that answers a
|
|
120
|
+
// hover with nothing.
|
|
121
|
+
const layout = readLayout();
|
|
122
|
+
|
|
123
|
+
const regionOpen = layout.indexOf('<ComposableRegion');
|
|
124
|
+
const regionClose = layout.indexOf('</ComposableRegion>');
|
|
125
|
+
const childrenIndex = layout.indexOf('{children}');
|
|
126
|
+
|
|
127
|
+
expect(regionOpen).toBeGreaterThan(-1);
|
|
128
|
+
expect(regionClose).toBeGreaterThan(-1);
|
|
129
|
+
// The region takes the live flow, not a copy of it.
|
|
130
|
+
expect(childrenIndex).toBeGreaterThan(regionOpen);
|
|
131
|
+
expect(childrenIndex).toBeLessThan(regionClose);
|
|
132
|
+
expect(layout).toMatch(
|
|
133
|
+
/<ComposableRegion[^>]*slug=\{PLACEHOLDER_SLUGS\.checkoutBody\}/
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// One slug, one CTA: the slot's own band steps aside for it.
|
|
137
|
+
expect(layout).toMatch(
|
|
138
|
+
/<ChromeSlot[^>]*PLACEHOLDER_SLUGS\.checkoutBody\b[^>]*emptyZoneBand=\{false\}[^>]*>/
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
maybeIt(
|
|
144
|
+
'keeps the dashed band on the two zones that have no live content',
|
|
145
|
+
() => {
|
|
146
|
+
// Nothing else renders in those zones, so removing their band would make
|
|
147
|
+
// them undiscoverable again — the exact problem the band exists for.
|
|
148
|
+
const layout = readLayout();
|
|
149
|
+
|
|
150
|
+
(['checkoutTop', 'checkoutBottom'] as const).forEach(
|
|
151
|
+
(key) => {
|
|
152
|
+
const slot = layout.match(
|
|
153
|
+
new RegExp(`<ChromeSlot[^>]*PLACEHOLDER_SLUGS\\.${key}\\b[^>]*>`)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(slot).not.toBeNull();
|
|
157
|
+
expect(slot![0]).not.toContain('emptyZoneBand');
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
maybeIt(
|
|
164
|
+
'no longer tells the designer the checkout body cannot be composed',
|
|
165
|
+
() => {
|
|
166
|
+
// The code-owned explainer said the flow "cannot be composed — use the
|
|
167
|
+
// zones above and below it". The body is composable now, so that sentence
|
|
168
|
+
// must not survive anywhere in this layout.
|
|
169
|
+
expect(readLayout()).not.toContain('CodeOwnedRegion');
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
maybeIt('explains the area it makes inert once it is composed', () => {
|
|
174
|
+
// The canvas click guard is keyed on being in the designer, not on the
|
|
175
|
+
// area being uncomposed. A composition that places no flow outlet leaves
|
|
176
|
+
// the real checkout visible and unclickable, so the region has to keep
|
|
177
|
+
// saying why.
|
|
178
|
+
expect(readLayout()).toMatch(/<ComposableRegion[^>]*composedNote=/);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
);
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The checkout outlet blocks style parts they do not render: the code-owned
|
|
6
|
+
* checkout teleports its markup into them, and each outlet declares the parts
|
|
7
|
+
* it HOSTS via an exported `HOSTED_PARTS` literal that the editor's
|
|
8
|
+
* descriptor conformance also reads. A manifest nobody stamps would put dead
|
|
9
|
+
* entries in the editor panel; a stamp nobody declares would be unstylable —
|
|
10
|
+
* this suite keeps declaration and reality pointing at each other:
|
|
11
|
+
*
|
|
12
|
+
* - every manifest name is stamped (`partAttrs('…')`) somewhere in the app's
|
|
13
|
+
* checkout views;
|
|
14
|
+
* - every stamped name is declared by at least one outlet manifest
|
|
15
|
+
* (`summary-*` legitimately by two: the mobile summary sheet re-renders the
|
|
16
|
+
* summary inside the flow subtree, and part rules are block-scoped);
|
|
17
|
+
* - the full vocabulary is pinned inline, so a rename fails loudly here
|
|
18
|
+
* before it silently orphans published compositions;
|
|
19
|
+
* - stamps are INERT (I4): `partAttrs` is the only stamping route, and no
|
|
20
|
+
* live runtime — app or pz-theme outside designer-only modules — queries
|
|
21
|
+
* `[data-part` from the DOM.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const repoRoot = path.resolve(__dirname, '../../../../..');
|
|
25
|
+
const blocksDir = path.resolve(__dirname, '..');
|
|
26
|
+
const pzThemeSrcDir = path.resolve(__dirname, '../..');
|
|
27
|
+
const appSrcDir = path.join(repoRoot, 'apps', 'projectzeronext', 'src');
|
|
28
|
+
const appCheckoutDir = path.join(appSrcDir, 'views', 'checkout');
|
|
29
|
+
|
|
30
|
+
const OUTLET_MANIFEST_FILES = [
|
|
31
|
+
'checkout-flow-outlet-block.tsx',
|
|
32
|
+
'checkout-steps-outlet-block.tsx',
|
|
33
|
+
'checkout-summary-outlet-block.tsx'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The reserved checkout part vocabulary. Changing it is a contract change:
|
|
38
|
+
* update the outlet manifests, the app stamps, the editor part groups AND
|
|
39
|
+
* this pin together — published compositions reference these names.
|
|
40
|
+
*/
|
|
41
|
+
const RESERVED_PART_NAMES = [
|
|
42
|
+
'agreement',
|
|
43
|
+
'continue-button',
|
|
44
|
+
'edit-button',
|
|
45
|
+
'payment-option',
|
|
46
|
+
'payment-option-icon',
|
|
47
|
+
'payment-options',
|
|
48
|
+
'payment-panel',
|
|
49
|
+
'place-order-button',
|
|
50
|
+
'section',
|
|
51
|
+
'section-header',
|
|
52
|
+
'section-number',
|
|
53
|
+
'section-title',
|
|
54
|
+
'step',
|
|
55
|
+
'step-icon',
|
|
56
|
+
'step-label',
|
|
57
|
+
'step-number',
|
|
58
|
+
'steps',
|
|
59
|
+
'summary',
|
|
60
|
+
'summary-edit',
|
|
61
|
+
'summary-header',
|
|
62
|
+
'summary-item',
|
|
63
|
+
'summary-row',
|
|
64
|
+
'summary-row-label',
|
|
65
|
+
'summary-row-value',
|
|
66
|
+
'summary-title',
|
|
67
|
+
'summary-total',
|
|
68
|
+
'summary-total-label',
|
|
69
|
+
'summary-total-value'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const PART_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
73
|
+
|
|
74
|
+
const readManifest = (fileName: string): string[] => {
|
|
75
|
+
const source = fs.readFileSync(path.join(blocksDir, fileName), 'utf8');
|
|
76
|
+
const hosted = /HOSTED_PARTS\s*(?::[^=]*)?=\s*\[([\s\S]*?)\]/.exec(source);
|
|
77
|
+
if (!hosted) return [];
|
|
78
|
+
|
|
79
|
+
return Array.from(hosted[1].matchAll(/'([^']+)'/g), (match) => match[1]);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const walkSources = (dir: string): string[] => {
|
|
83
|
+
const files: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
86
|
+
const full = path.join(dir, entry.name);
|
|
87
|
+
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
if (entry.name === 'node_modules') continue;
|
|
90
|
+
files.push(...walkSources(full));
|
|
91
|
+
} else if (/\.(ts|tsx)$/.test(entry.name)) {
|
|
92
|
+
files.push(full);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return files;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const isTestFile = (file: string): boolean =>
|
|
100
|
+
/__tests__/.test(file) || /\.test\.(ts|tsx)$/.test(file);
|
|
101
|
+
|
|
102
|
+
const manifests = Object.fromEntries(
|
|
103
|
+
OUTLET_MANIFEST_FILES.map((file) => [file, readManifest(file)])
|
|
104
|
+
) as Record<string, string[]>;
|
|
105
|
+
|
|
106
|
+
const manifestNames = new Set(Object.values(manifests).flat());
|
|
107
|
+
|
|
108
|
+
// Runtime sources only. A test may legitimately write a part name into a
|
|
109
|
+
// string template or assert on a raw attribute; what this suite pins is what
|
|
110
|
+
// the SHIPPED checkout markup stamps.
|
|
111
|
+
const checkoutSources = walkSources(appCheckoutDir)
|
|
112
|
+
.filter((file) => !isTestFile(file))
|
|
113
|
+
.map((file) => ({
|
|
114
|
+
file: path.relative(repoRoot, file),
|
|
115
|
+
source: fs.readFileSync(file, 'utf8')
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const stampedNames = new Set(
|
|
119
|
+
checkoutSources.flatMap(({ source }) =>
|
|
120
|
+
Array.from(source.matchAll(/partAttrs\(\s*'([^']+)'/g), (m) => m[1])
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
describe('checkout hosted-parts manifests', () => {
|
|
125
|
+
it('finds a non-empty HOSTED_PARTS literal in each outlet block', () => {
|
|
126
|
+
for (const file of OUTLET_MANIFEST_FILES) {
|
|
127
|
+
expect(manifests[file].length).toBeGreaterThan(0);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses only well-formed kebab-case part names, everywhere', () => {
|
|
132
|
+
for (const name of [
|
|
133
|
+
...Array.from(manifestNames),
|
|
134
|
+
...Array.from(stampedNames)
|
|
135
|
+
]) {
|
|
136
|
+
expect(name).toMatch(PART_NAME_PATTERN);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('pins the full reserved vocabulary — renames fail loudly', () => {
|
|
141
|
+
expect(Array.from(manifestNames).sort()).toEqual(RESERVED_PART_NAMES);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('declares every stamped part in at least one outlet manifest', () => {
|
|
145
|
+
const undeclared = Array.from(stampedNames).filter(
|
|
146
|
+
(name) => !manifestNames.has(name)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(undeclared).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('stamps every declared part at least once in the checkout views', () => {
|
|
153
|
+
const unstamped = Array.from(manifestNames).filter(
|
|
154
|
+
(name) => !stampedNames.has(name)
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(unstamped).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('checkout part stamps are inert (I4)', () => {
|
|
162
|
+
it('routes every app stamp through partAttrs — no raw data-part attribute', () => {
|
|
163
|
+
const offenders = checkoutSources
|
|
164
|
+
.filter(({ source }) => source.includes('data-part='))
|
|
165
|
+
.map(({ file }) => file);
|
|
166
|
+
|
|
167
|
+
expect(offenders).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// The attributes are a styling contract, not a runtime signal: nothing on
|
|
171
|
+
// the live path may branch on them. Designer-only modules (the queries are
|
|
172
|
+
// gated behind the editor-proven designer flag) are the sole exemption.
|
|
173
|
+
const DESIGNER_ONLY_PZ_THEME_FILES = [
|
|
174
|
+
path.join('hooks', 'use-designer-features.tsx'),
|
|
175
|
+
path.join('components', 'part-state-preview.tsx'),
|
|
176
|
+
path.join('blocks', 'checkout-outlet-base.tsx')
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const PART_QUERY_PATTERN =
|
|
180
|
+
/(?:querySelectorAll|querySelector|closest|matches)\s*(?:<[^>\n]*>)?\s*\(\s*[`'"]\[data-part/;
|
|
181
|
+
|
|
182
|
+
it('never queries [data-part from the app runtime', () => {
|
|
183
|
+
const offenders = walkSources(appSrcDir)
|
|
184
|
+
.filter((file) => !isTestFile(file))
|
|
185
|
+
.filter((file) => PART_QUERY_PATTERN.test(fs.readFileSync(file, 'utf8')))
|
|
186
|
+
.map((file) => path.relative(repoRoot, file));
|
|
187
|
+
|
|
188
|
+
expect(offenders).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('never queries [data-part from pz-theme outside designer-only modules', () => {
|
|
192
|
+
const offenders = walkSources(pzThemeSrcDir)
|
|
193
|
+
.filter((file) => !isTestFile(file))
|
|
194
|
+
.filter((file) => {
|
|
195
|
+
const relative = path.relative(pzThemeSrcDir, file);
|
|
196
|
+
return !DESIGNER_ONLY_PZ_THEME_FILES.includes(relative);
|
|
197
|
+
})
|
|
198
|
+
.filter((file) => PART_QUERY_PATTERN.test(fs.readFileSync(file, 'utf8')))
|
|
199
|
+
.map((file) => path.relative(repoRoot, file));
|
|
200
|
+
|
|
201
|
+
expect(offenders).toEqual([]);
|
|
202
|
+
});
|
|
203
|
+
});
|