@wix/origami-lint 1.34.24 → 1.34.25

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.
@@ -14,15 +14,21 @@
14
14
  // - a <Remove> holds exactly one component element (CONV-parseJsx.ts:1028), and no <Remove> sits inside another: <Remove> wraps
15
15
  // the component itself (CONV-parseJsx.ts:1237; converter 0.86 parseJsx.ts:1500, :1681);
16
16
  // - only elements go between a component's tags: no text, no {…}, no fragment (CONV-parseJsx.ts:1668);
17
- // - an <InternalContainer>'s backgroundLayer / pinnedLayer is an array of elements (CONV-parseJsx.ts:1630).
18
- // Wrappers are transparent for the tree, as they are for the import: a section wrapped in <Remove> is still the page's child.
17
+ // - an <InternalContainer>'s backgroundLayer / pinnedLayer, and a <Page>'s backgroundLayer / viewportLayer, is an array of elements
18
+ // (CONV-parseJsx.ts:1630);
19
+ // - a <PinnedToScreen> takes no props and holds one section, header or footer, in its <Page> slot: a child of the <Page>, or its
20
+ // header / footer. A pinned section is never in the viewportLayer, which holds the page's other pinned components.
21
+ // Wrappers are transparent for the tree, as they are for the import: a section wrapped in <Remove> or <PinnedToScreen> is still the
22
+ // page's child.
19
23
  // <Interaction> is no wrapper since converter 0.80 (interactions live in servicesConfigs/interactions.ts): allowed-imports
20
24
  // reports the tag, and it stays transparent here so the elements inside it aren't reported too. Context-provider wrappers join
21
25
  // once the page model lists the providers.
22
26
  import { FILE_START } from "../../page-model/index.js";
23
- import { LAYER_ATTRIBUTES, SECTIONS, WRAPPERS } from "../../rules-api/index.js";
27
+ import { getName, GLOBAL_SECTIONS, LAYER_ATTRIBUTES, SECTIONS, WRAPPERS } from "../../rules-api/index.js";
24
28
  import { defineRule } from "./define-rule.js";
25
- const CHECKS = ['missing_root', 'invalid_global_root', 'missing_root_id', 'invalid_page_root', 'non_element_child', 'invalid_section_container', 'nested_page', 'landmark_inside_container', 'invalid_wrapper_children', 'nested_remove', 'invalid_element_array', 'invalid_landmark_section'];
29
+ const CHECKS = ['missing_root', 'invalid_global_root', 'missing_root_id', 'invalid_page_root', 'non_element_child', 'invalid_section_container', 'nested_page', 'landmark_inside_container', 'invalid_wrapper_children', 'nested_remove', 'invalid_element_array', 'invalid_landmark_section', 'invalid_pinned_child', 'invalid_pinned_placement', 'pinned_wrapper_props', 'section_in_viewport_layer'];
30
+ /** What <PinnedToScreen> may hold: a section, a header or footer, or a page's reference to a global one. */
31
+ const PINNABLE = [...SECTIONS, ...GLOBAL_SECTIONS, 'headerLandmark', 'footerLandmark'];
26
32
  export default defineRule({
27
33
  name: 'page-structure',
28
34
  checks: CHECKS,
@@ -33,7 +39,10 @@ export default defineRule({
33
39
  if (!input.rulesApi.getComponent(input.node))
34
40
  return;
35
41
  if (isWrapper(input.node, input.rulesApi)) {
36
- if (input.rulesApi.getType(input.node) !== 'remove')
42
+ const type = input.rulesApi.getType(input.node);
43
+ if (type === 'pinnedToScreen')
44
+ checkPinnedToScreen(input);
45
+ if (type !== 'remove')
37
46
  return;
38
47
  holdsOneElement(input);
39
48
  checkNoRemoveInRemove(input);
@@ -45,6 +54,7 @@ export default defineRule({
45
54
  if (isSection(input.node, input.rulesApi))
46
55
  hasOneInternalContainer(input.node, input);
47
56
  hasLayerArrays(input);
57
+ checkViewportLayerItems(input);
48
58
  isNotNestedPage(input);
49
59
  isOutsideInternalContainers(input);
50
60
  if (!isTop(input.node, input.rulesApi))
@@ -162,9 +172,10 @@ function hasOnlyElements({ node, rulesApi, report }) {
162
172
  report({ check: 'non_element_child', loc: child.type === 'JSXText' ? getTextLoc(child) : child.loc, message: `${describeElement(node, rulesApi)} has text or {…} as a child. Put the value in an attribute: only elements go between the tags (tracker CONV-parseJsx.ts:1668).` });
163
173
  }
164
174
  }
165
- /** Reports an <InternalContainer>'s backgroundLayer / pinnedLayer that isn't an array literal of elements (CONV-parseJsx.ts:1630). */
175
+ /** Reports an <InternalContainer>'s or a <Page>'s layer that isn't an array literal of elements (CONV-parseJsx.ts:1630). */
166
176
  function hasLayerArrays({ node, rulesApi, report }) {
167
- if (rulesApi.getType(node) !== 'innerElement')
177
+ const type = rulesApi.getType(node);
178
+ if (type !== 'innerElement' && type !== 'page')
168
179
  return;
169
180
  for (const attribute of node.openingElement.attributes) {
170
181
  if (attribute.type !== 'JSXAttribute' || attribute.name.type !== 'JSXIdentifier' || !LAYER_ATTRIBUTES.includes(attribute.name.name))
@@ -174,7 +185,62 @@ function hasLayerArrays({ node, rulesApi, report }) {
174
185
  report({ check: 'invalid_element_array', loc: attribute.loc, message: `${describeElement(node, rulesApi)}'s ${attribute.name.name} must be an array of elements: ${attribute.name.name}={[<… />]} (tracker CONV-parseJsx.ts:1630).` });
175
186
  }
176
187
  }
188
+ /** A <PinnedToScreen>: no props, one element held, a pinnable one, in its <Page> slot. */
189
+ function checkPinnedToScreen(input) {
190
+ const { node, rulesApi, report } = input;
191
+ if (node.openingElement.attributes.length > 0) {
192
+ report({ check: 'pinned_wrapper_props', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> takes no props: the element it pins keeps its own id, className and layout.` });
193
+ }
194
+ holdsOneElement(input);
195
+ checkPinnedChild(input);
196
+ checkPinnedPlacement(input);
197
+ }
198
+ /** Reports a <PinnedToScreen> whose one element isn't a section, header or footer; the wrong number of elements is holdsOneElement's. */
199
+ function checkPinnedChild({ node, rulesApi, report }) {
200
+ if (!holdsOne(node))
201
+ return;
202
+ const inside = rulesApi.getChildElements(node);
203
+ if (inside.length === 1 && isPinnable(inside[0], rulesApi))
204
+ return;
205
+ report({ check: 'invalid_pinned_child', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> holds ${inside.map((child) => describeElement(child, rulesApi)).join(', ') || 'nothing'}. It pins a section, header or footer; put any other pinned component in the <Page>'s viewportLayer.` });
206
+ }
207
+ /** Reports a <PinnedToScreen> anywhere but in a <Page>'s children or its header / footer, or inside another <PinnedToScreen>. */
208
+ function checkPinnedPlacement({ node, rulesApi, report }) {
209
+ const holder = getHolder(node, rulesApi);
210
+ const between = getAncestors(node).slice(0, holder ? getAncestors(node).indexOf(holder) : undefined);
211
+ const attribute = holder && getHolderAttribute(node, holder);
212
+ if (holder && rulesApi.getType(holder) === 'page' && (attribute === null || attribute === 'header' || attribute === 'footer')
213
+ && !between.some((wrapper) => rulesApi.getType(wrapper) === 'pinnedToScreen'))
214
+ return;
215
+ const where = holder ? `inside ${describeElement(holder, rulesApi)}${attribute ? `'s ${attribute}` : ''}` : 'at the top';
216
+ report({ check: 'invalid_pinned_placement', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> is ${where}. It goes in its <Page> slot only, once: around a child of the <Page>, or in its header or footer.` });
217
+ }
218
+ /** Reports a section, header or footer in a <Page>'s viewportLayer: a pinned one stays in its slot, wrapped in <PinnedToScreen>. One already wrapped is checkPinnedPlacement's. */
219
+ function checkViewportLayerItems({ node, rulesApi, report }) {
220
+ if (rulesApi.getType(node) !== 'page')
221
+ return;
222
+ for (const { element, attribute } of rulesApi.getAttributeElements(node)) {
223
+ if (getName(attribute) !== 'viewportLayer' || !isPinnable(element, rulesApi))
224
+ continue;
225
+ if (getAncestors(element).some((wrapper) => rulesApi.getType(wrapper) === 'pinnedToScreen'))
226
+ continue;
227
+ report({ check: 'section_in_viewport_layer', loc: getLoc(element, rulesApi), message: `${describeElement(element, rulesApi)} is in the viewportLayer of ${describeElement(node, rulesApi)}. A pinned section, header or footer stays in its <Page> slot, wrapped in <PinnedToScreen>.` });
228
+ }
229
+ }
177
230
  // ── reading the tree ──
231
+ /** A section, header or footer, which <PinnedToScreen> may hold. */
232
+ const isPinnable = (element, rulesApi) => {
233
+ const type = rulesApi.getType(element);
234
+ return type !== null && PINNABLE.includes(type);
235
+ };
236
+ /** The attribute of the holder the element is passed in (`header`); null for one of its children. */
237
+ function getHolderAttribute(element, holder) {
238
+ for (let node = element.parent; node && node !== holder; node = node.parent) {
239
+ if (node.type === 'JSXAttribute')
240
+ return getName(node);
241
+ }
242
+ return null;
243
+ }
178
244
  /** 'header' / 'footer' for the section tags and for a page's reference to the global ones; null for anything else. */
179
245
  function headerOrFooter(type) {
180
246
  if (type === 'header' || type === 'globalHeader' || type === 'headerLandmark')
@@ -2,8 +2,8 @@
2
2
  export const SECTIONS = ['section', 'header', 'footer'];
3
3
  /** A page's reference to a global component (`<HeaderSection_x sharedCompId="…" />`). */
4
4
  export const GLOBAL_SECTIONS = ['globalHeader', 'globalFooter', 'globalSection'];
5
- /** A tag that wraps one component and isn't one itself: `<Remove when={…}>`, `<Interaction interactKey="…">`. */
6
- export const WRAPPERS = ['remove', 'interaction'];
5
+ /** A tag that wraps one component and isn't one itself: `<Remove when={…}>`, `<Interaction interactKey="…">`, `<PinnedToScreen>` (the import drops it). */
6
+ export const WRAPPERS = ['remove', 'interaction', 'pinnedToScreen'];
7
7
  /** The name a structure file exports its tree under: `export const Structure = () => (…)`. */
8
8
  export const STRUCTURE = 'Structure';
9
9
  /** The props a global component's Structure may take: its instance identity, `({ id, className })` (#300). */
@@ -38,6 +38,7 @@ export const EVENT_ATTRIBUTE = /^on/i;
38
38
  /** A callback prop, which the runtime hands a function: `on` and a capital letter, `onClick` but not `onclick` (the runtime's withOrigami.tsx). */
39
39
  export const CALLBACK_PROP = /^on[A-Z]/;
40
40
  /** By import source; the wrappers also by the imported name, since their packages export other things (`Show`, `Interact`). */
41
- export const LAYER_ATTRIBUTES = ['backgroundLayer', 'pinnedLayer'];
41
+ /** The attributes that hold an element's layers: drawn behind it, and pinned to it (a container's pinnedLayer, the Page's viewportLayer). */
42
+ export const LAYER_ATTRIBUTES = ['backgroundLayer', 'pinnedLayer', 'viewportLayer'];
42
43
  /** The attribute that holds the elements of a component's inner parts: `elementProps={{ panel: { content: <X /> } }}`. */
43
44
  export const ELEMENT_PROPS = 'elementProps';
@@ -4,6 +4,7 @@ const TYPE_BY_IMPORT_SOURCE = [
4
4
  [new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?Page$`), 'page'],
5
5
  [new RegExp(`^${SYSTEM_PACKAGE}/layout/Header$`), 'headerLandmark'],
6
6
  [new RegExp(`^${SYSTEM_PACKAGE}/layout/Footer$`), 'footerLandmark'],
7
+ [new RegExp(`^${SYSTEM_PACKAGE}/layout/PinnedToScreen$`), 'pinnedToScreen'],
7
8
  [new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?Section$`), 'section'],
8
9
  [new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?HeaderSection$`), 'header'],
9
10
  [new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?FooterSection$`), 'footer'],
@@ -37,7 +38,7 @@ export function getType(element, program) {
37
38
  return imported === undefined || found.imported === imported ? type : null;
38
39
  }
39
40
  /** The tags the import reads as the DM's own components ('document') or a global component's reference (identity.ts identityOfTag): no data attributes. */
40
- const NO_DATA_TYPES = ['page', 'section', 'header', 'footer', 'headerLandmark', 'footerLandmark', ...GLOBAL_SECTIONS];
41
+ const NO_DATA_TYPES = ['page', 'section', 'header', 'footer', 'headerLandmark', 'footerLandmark', 'pinnedToScreen', ...GLOBAL_SECTIONS];
41
42
  /** Converter 0.81: are the tag's attributes saved as its component's data? Yes on a builder component (its import gives a componentType):
42
43
  * imported, not from `document` and not from a local file. No on the DM's own tags (`Page`, the sections) and a global component's reference. */
43
44
  export function hasDataAttributes(element, program) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.24",
3
+ "version": "1.34.25",
4
4
  "description": "Offline lint for an exported Origami site: lints the pages that changed since the last passing run",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,5 +52,5 @@
52
52
  ]
53
53
  }
54
54
  },
55
- "falconPackageHash": "1bb53e289c65cf4acdcbc54a85fcf823b282d0e1be7bac656dff5170"
55
+ "falconPackageHash": "e4031adeef0d38fd736d432532a85f648531c1c89cff31aa089a0fad"
56
56
  }