@wix/origami-lint 1.34.22 → 1.34.24

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.
@@ -10,9 +10,10 @@ import globalStyles from "./site-app/global-styles.js";
10
10
  import noAbsoluteSection from "./site-app/no-absolute-section.js";
11
11
  import pageStructure from "./site-app/page-structure.js";
12
12
  import slotAttributes from "./site-app/slot-attributes.js";
13
+ import slotPlacement from "./site-app/slot-placement.js";
13
14
  import themeFontIds from "./site-app/theme-font-ids.js";
14
15
  import themeRoot from "./site-app/theme-root.js";
15
16
  import noFreeCode from "./site-app/no-free-code.js";
16
17
  // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
17
- export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors];
18
+ export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement];
18
19
  export { defineRule } from "./site-app/define-rule.js";
@@ -2,7 +2,7 @@
2
2
  // 1227, 1231, 1235, 1255, 1268, 1289, 1697; projectData.ts 214, 225, 281, 435, 443; enrichImage.ts 94, 105.
3
3
  // Converter 0.81 (checked at 0.84, 2c0e21c): the tag's import, not a `type` attr, decides if a component takes data attributes; `type` is data.
4
4
  import { isDeepStrictEqual } from 'node:util';
5
- import { DATA_KEY, EVENT_ATTRIBUTE, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getProperty, isBound, isOneEditAway, isReservedAttribute, isStructure, ownerOf, } from "../../rules-api/index.js";
5
+ import { DATA_KEY, EVENT_ATTRIBUTE, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getProperty, isBound, isOneEditAway, isReadByImport, isReservedAttribute, ownerOf, } from "../../rules-api/index.js";
6
6
  import { defineRule } from "./define-rule.js";
7
7
  const CHECKS = ['attribute_without_braces', 'non_literal_data', 'duplicate_element_prop', 'invalid_element_props_object', 'invalid_element_props_entry', 'invalid_data_object', 'invalid_element_props_key', 'unsupported_element_props_key', 'mixed_data_wrapper', 'duplicate_data_key', 'bare_document_href', 'missing_document_id', 'inconsistent_image_url', 'misplaced_image_url_call'];
8
8
  /** The names the import never reads as data (dataKeys.ts). The React name is joined as the converter does: Wix Standards fails the build on it in any file. */
@@ -43,7 +43,7 @@ export default defineRule({
43
43
  // ── the callback's steps ──
44
44
  /** A component written out under Structure: its data values, then what the import builds from the ones it read. */
45
45
  function checkJSXElementDataValues(input) {
46
- if (!input.rulesApi.getComponent(input.node) || !isWrittenOut(input.node))
46
+ if (!input.rulesApi.getComponent(input.node) || !isReadByImport(input.node))
47
47
  return;
48
48
  const elementProps = getAttribute(input.node, 'elementProps');
49
49
  const keysOnce = !elementProps || checkEntryKeysOnce(input, elementProps);
@@ -491,14 +491,3 @@ function dataValues(input) {
491
491
  }
492
492
  return values;
493
493
  }
494
- /** In Structure's own JSX, or in a renderItem template, which the import reads in the row's scope (parseJsx.ts claimSlotChild). */
495
- function isWrittenOut(element) {
496
- for (let up = element.parent; up.type !== 'JSXElement' && up.type !== 'Program'; up = up.parent) {
497
- if (up.type === 'ArrowFunctionExpression')
498
- return isStructure(up) || getCallbackKind(up) === 'template';
499
- if (NOT_READ_IN.includes(up.type) || (up.type === 'JSXExpressionContainer' && up.parent.type !== 'JSXAttribute'))
500
- return false;
501
- }
502
- return true;
503
- }
504
- const NOT_READ_IN = ['FunctionExpression', 'CallExpression', 'ConditionalExpression', 'LogicalExpression'];
@@ -5,7 +5,7 @@
5
5
  // - the root element has no `slot=`: it has no owner to be a slot of (CONV-fromBundle.ts:1312);
6
6
  // - two children of one owner don't share a slot key (CONV-slots.ts:555).
7
7
  // Children are the element children and the layer elements (backgroundLayer, pinnedLayer); <Remove> and <Interaction> are transparent.
8
- import { LAYER_ATTRIBUTES, WRAPPERS } from "../../rules-api/index.js";
8
+ import { ELEMENT_PROPS, LAYER_ATTRIBUTES, WRAPPERS } from "../../rules-api/index.js";
9
9
  import { defineRule } from "./define-rule.js";
10
10
  const CHECKS = ['root_has_slot', 'attribute_child_has_slot', 'mixed_slot_forms', 'duplicate_slot'];
11
11
  export default defineRule({
@@ -78,13 +78,13 @@ const getSlottedChildren = (owner, rulesApi) => [...rulesApi.getChildElements(ow
78
78
  .filter((child) => rulesApi.getSlot(child) !== null);
79
79
  /** `elementProps={{ panel: { slots: {…} } }}`, or the same in a nested `elementProps` of an entry. */
80
80
  function hasEntrySlots(owner) {
81
- const attribute = owner.openingElement.attributes.find((candidate) => candidate.type === 'JSXAttribute' && getAttributeName(candidate) === 'elementProps');
81
+ const attribute = owner.openingElement.attributes.find((candidate) => candidate.type === 'JSXAttribute' && getAttributeName(candidate) === ELEMENT_PROPS);
82
82
  return attribute?.type === 'JSXAttribute' && attribute.value?.type === 'JSXExpressionContainer' && entriesHaveSlots(attribute.value.expression);
83
83
  }
84
84
  function entriesHaveSlots(entries) {
85
85
  if (entries.type !== 'ObjectExpression')
86
86
  return false;
87
- return entries.properties.some((entry) => entry.type === 'Property' && entry.value.type === 'ObjectExpression' && entry.value.properties.some((property) => property.type === 'Property' && (getKeyName(property) === 'slots' || (getKeyName(property) === 'elementProps' && entriesHaveSlots(property.value)))));
87
+ return entries.properties.some((entry) => entry.type === 'Property' && entry.value.type === 'ObjectExpression' && entry.value.properties.some((property) => property.type === 'Property' && (getKeyName(property) === 'slots' || (getKeyName(property) === ELEMENT_PROPS && entriesHaveSlots(property.value)))));
88
88
  }
89
89
  const getKeyName = (property) => property.computed ? null : property.key.type === 'Identifier' ? property.key.name : property.key.type === 'Literal' ? String(property.key.value) : null;
90
90
  const getAttributeName = (attribute) => (attribute.name.type === 'JSXIdentifier' ? String(attribute.name.name) : '');
@@ -0,0 +1,107 @@
1
+ import { DATA_KEY, ELEMENT_PROPS, isRecord, isReadByImport, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, SHARED_COMP_ID } from "../../rules-api/index.js";
2
+ import { defineRule } from "./define-rule.js";
3
+ const CHECKS = ['attribute_slot_place', 'element_props_slot_place', 'slot_owner_data_object'];
4
+ /** The names the import keeps for itself, so a slot can't sit under one: the converter's EMITTED_ATTR_NAMES. */
5
+ const RESERVED_KEYS = new Set([...RESERVED_ATTRIBUTES, SHARED_COMP_ID, 'backgroundLayer']);
6
+ /** An inner part's name: a safe CSS identifier, and not an object key the import keeps out. Copied from the DM converter's isSafeCssIdentifier: https://github.com/wix-private/document-management/blob/e1d9b91f44d7ea8d6c1203dbfd26ee58a7f7079f/document-management-jsx-converter/src/css-codec/css/cssSelectors.ts#L47-L50 */
7
+ const PART_NAME = /^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/;
8
+ const UNSAFE_PART_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
9
+ export default defineRule({
10
+ name: 'slot-placement',
11
+ checks: CHECKS,
12
+ editors: ['studio2'],
13
+ callbacks: {
14
+ JSXElement: (input) => {
15
+ if (!input.rulesApi.getComponent(input.node) || !isReadByImport(input.node))
16
+ return;
17
+ const slots = input.rulesApi.getAttributeElements(input.node).filter(({ name }) => !LAYER_ATTRIBUTES.includes(name));
18
+ checkSlotPlaces(slots, input);
19
+ checkSlotOwnerData(slots, input);
20
+ },
21
+ },
22
+ });
23
+ /** Reports each element at a place the import can't write back (CONV-parseJsx.ts:1532, :1571 and the repeats at CONV-fromBundle.ts:1247, :1271, :143). */
24
+ function checkSlotPlaces(slots, { node, rulesApi, report }) {
25
+ for (const { element, name, path } of slots) {
26
+ const value = rulesApi.getAttributeValue(node, name);
27
+ const place = getPartPlace(path);
28
+ if (name === ELEMENT_PROPS ? isPartPlaceWritable(place, value) : isAttributePlaceWritable(name, path, value))
29
+ continue;
30
+ const check = name === ELEMENT_PROPS ? 'element_props_slot_place' : 'attribute_slot_place';
31
+ const message = name === ELEMENT_PROPS
32
+ ? `${describe(node, rulesApi)} holds an element in elementProps at a place the import can't write back: put it at a plain data key of the inner part (${place.parts.at(-1) ?? 'partName'}: {content: <…/>}) (tracker CONV-parseJsx.ts:1571).`
33
+ : `${describe(node, rulesApi)} holds an element in '${name}' at a place the import can't write back: put it under plain-name keys, give each array item on the way its own string _id, and no . [ ] in keys (tracker CONV-parseJsx.ts:1532).`;
34
+ report({ check, loc: getLoc(element, rulesApi), message });
35
+ }
36
+ }
37
+ /** True when every key on the way is a plain name and the attribute has no ambiguous key. An element in `data` (`data={{ items: […] }}`) is read from inside it: its keys are its own, not `data`. */
38
+ function isAttributePlaceWritable(name, path, value) {
39
+ if (!path)
40
+ return false;
41
+ const keys = name === 'data' ? path.slice(1) : path;
42
+ if (name === 'data' && (path[0].itemId !== undefined || keys.length === 0))
43
+ return false;
44
+ return keys.every(({ key }) => isPlainKey(key)) && !isAmbiguous(value);
45
+ }
46
+ /** The inner parts on the way (`elementProps` inside an entry opens the parts of that entry), as far as the path reads, and the steps inside the entry's data (or after the entry, when it has no data) down to the element. keys is null when the way has no such form. */
47
+ function getPartPlace(path) {
48
+ const parts = [];
49
+ if (!path || path[0].itemId !== undefined)
50
+ return { parts, keys: null };
51
+ let rest = path.slice(1);
52
+ while (rest.length) {
53
+ const [part, step, ...more] = rest;
54
+ parts.push(part.key);
55
+ if (part.itemId !== undefined || !step)
56
+ return { parts, keys: null };
57
+ if (step.key === ELEMENT_PROPS && step.itemId === undefined) {
58
+ rest = more;
59
+ continue;
60
+ }
61
+ return { parts, keys: step.key === 'data' && step.itemId === undefined ? more : rest.slice(1) };
62
+ }
63
+ return { parts, keys: null };
64
+ }
65
+ /** True when the element sits at a data key of an inner part, under plain keys, with none of the entry's own keys (`slots`, `data`…) first or last, and the part names can be written. In a wrapper-shaped `data` (see isWrapperEntry) the keys after the wrapper's own `data` key are the ones judged. */
66
+ function isPartPlaceWritable({ parts, keys }, value) {
67
+ if (!keys?.length || !parts.every(isPartName) || isAmbiguous(value))
68
+ return false;
69
+ const own = keys[0].key === 'data' && isWrapperEntry(value, parts) ? keys.slice(1) : keys;
70
+ return own.length > 0 && own.every(({ key }) => isPlainKey(key)) && !RESERVED_ENTRY_KEYS.includes(own[0].key) && !RESERVED_ENTRY_KEYS.includes(own.at(-1).key);
71
+ }
72
+ /** True when the entry's `data` could be a canonical wrapper: the parts of the DM converter's isCanonicalDataWrapper that do not need the part's component type (it also wants `type` to be `<ComponentType>.Data`): https://github.com/wix-private/document-management/blob/e1d9b91f44d7ea8d6c1203dbfd26ee58a7f7079f/document-management-jsx-converter/src/projectData.ts#L304-L321. The import tells it by that type (parseJsx.ts:544), which the lint does not know, so an element inside such a `data.data` is not reported: the converter may read it. */
73
+ function isWrapperEntry(value, parts) {
74
+ let entry = value;
75
+ for (const [index, part] of parts.entries()) {
76
+ const holder = index === 0 ? entry : isRecord(entry) ? entry.elementProps : undefined;
77
+ entry = isRecord(holder) ? holder[part] : undefined;
78
+ }
79
+ const data = isRecord(entry) ? entry.data : undefined;
80
+ return isRecord(data) && typeof data.type === 'string' && data.type.endsWith('.Data') && (data.id === undefined || typeof data.id === 'string') && isRecord(data.data)
81
+ && Object.keys(data).every((key) => key === 'type' || key === 'id' || key === 'data');
82
+ }
83
+ /** Reports a component that holds a slot element in its attributes while its data is not an object: a string, number, boolean, array or null (CONV-fromBundle.ts:127). */
84
+ function checkSlotOwnerData(slots, { node, rulesApi, report }) {
85
+ const first = slots.find(({ name }) => name !== ELEMENT_PROPS);
86
+ const data = rulesApi.getAttributeValue(node, 'data');
87
+ if (!first || data === undefined || isRecord(data))
88
+ return;
89
+ report({ check: 'slot_owner_data_object', loc: getLoc(node, rulesApi), message: `${describe(node, rulesApi)} holds a slot element (in '${first.name}'), so its data must be an object: write data={{…}} or leave data out, not data={${JSON.stringify(data)}} (tracker CONV-fromBundle.ts:127).` });
90
+ }
91
+ const isPartName = (name) => PART_NAME.test(name) && !UNSAFE_PART_NAMES.has(name);
92
+ const isPlainKey = (key) => DATA_KEY.test(key) && !RESERVED_KEYS.has(key);
93
+ /** A key with . [ or ], or two array items with one `_id`, anywhere in the value: the import can't tell their paths apart (the DM converter's hasAmbiguousKey: https://github.com/wix-private/document-management/blob/e1d9b91f44d7ea8d6c1203dbfd26ee58a7f7079f/document-management-jsx-converter/src/slots.ts#L188-L199). */
94
+ function isAmbiguous(value) {
95
+ if (Array.isArray(value)) {
96
+ const ids = value.map((item) => (isRecord(item) ? item._id : undefined)).filter((id) => typeof id === 'string');
97
+ return new Set(ids).size !== ids.length || value.some(isAmbiguous);
98
+ }
99
+ return isRecord(value) && Object.entries(value).some(([key, inner]) => /[.[\]]/.test(key) || isAmbiguous(inner));
100
+ }
101
+ /** Where to mark an element: its id attribute, or its tag name. */
102
+ const getLoc = (element, rulesApi) => rulesApi.getComponent(element)?.loc ?? element.openingElement.name.loc;
103
+ /** '<Slideshow> comp-2', or '<Top>' without an id. */
104
+ function describe(element, rulesApi) {
105
+ const id = rulesApi.getId(element);
106
+ return `<${rulesApi.getTagName(element)}>${id ? ` ${id}` : ''}`;
107
+ }
@@ -1,6 +1,6 @@
1
1
  // Part 3 — rules API: one linted file's page and the functions over it, and how a rule reports.
2
2
  export { createRulesApi, getOrCreateRulesApi } from "./rules-api.js";
3
3
  export { createCssFileReporter, describeAtRule, reportCssProblem } from "./report.js";
4
- export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isReservedAttribute, isStructure, ownerOf, } from "../shared/utils.js";
5
- export { BUILDERS, CALLBACK_PROP, DATA_KEY, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
4
+ export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isRecord, isReservedAttribute, isStructure, isReadByImport, ownerOf, } from "../shared/utils.js";
5
+ export { BUILDERS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, LAYER_ATTRIBUTES, RESERVED_ATTRIBUTES, RESERVED_ENTRY_KEYS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE, SECTIONS, SHARED_COMP_ID, STRUCTURE, SYSTEM_PACKAGE, TRANSFORM_BUILDERS, TYPED_BUILDERS, WRAPPERS, } from "../shared/constants.js";
6
6
  export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readSegment } from "../shared/css.js";
@@ -5,7 +5,7 @@ import path from 'node:path';
5
5
  import { GLOBAL_COMPONENTS_FOLDER } from "../shared/site-files-tree-mapping.js";
6
6
  import { models, pageId } from "../bi/stats.js";
7
7
  import { readPage } from "../page-model/index.js";
8
- import { getAttributeElements as getHeldElements, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
8
+ import { getAttributeElements as getHeldElements, getAttributeValue, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
9
9
  import { readBreakpointName, readCustomMedia, readThemeTokens } from "../shared/css.js";
10
10
  import { WRAPPERS } from "../shared/constants.js";
11
11
  import { getEnclosingAtRules, isBaseDeclaration } from "./utils.js";
@@ -25,7 +25,7 @@ export function createRulesApi(page) {
25
25
  return [];
26
26
  return isWrapper(child) ? getChildElements(child) : [child];
27
27
  });
28
- const getAttributeElements = (node) => getHeldElements(node).flatMap(({ element, attribute }) => (isWrapper(element) ? getChildElements(element).map((inner) => ({ element: inner, attribute })) : [{ element, attribute }]));
28
+ const getAttributeElements = (node) => getHeldElements(node).flatMap((held) => (isWrapper(held.element) ? getChildElements(held.element).map((inner) => ({ ...held, element: inner })) : [held]));
29
29
  return {
30
30
  page,
31
31
  // A JSX element starts where its component starts.
@@ -44,6 +44,7 @@ export function createRulesApi(page) {
44
44
  getChildren,
45
45
  getChildElements,
46
46
  getAttributeElements,
47
+ getAttributeValue,
47
48
  getSlot,
48
49
  getImport: (node) => getImport(node.type === 'JSXElement' ? getTagName(node).split('.')[0] : node.name, getProgram(node)),
49
50
  hasDataAttributes: (node) => hasDataAttributes(node, getProgram(node)),
@@ -39,3 +39,5 @@ export const EVENT_ATTRIBUTE = /^on/i;
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
41
  export const LAYER_ATTRIBUTES = ['backgroundLayer', 'pinnedLayer'];
42
+ /** The attribute that holds the elements of a component's inner parts: `elementProps={{ panel: { content: <X /> } }}`. */
43
+ export const ELEMENT_PROPS = 'elementProps';
@@ -89,20 +89,66 @@ export function getId(element) {
89
89
  export function getSlot(element) {
90
90
  return getLiteralString(element.openingElement.attributes.find(isSlotAttribute)?.value);
91
91
  }
92
- /** The attributes that hold an element's layers: drawn behind it, and pinned to it. */
93
- /** The elements this one holds in its attributes (`content={<X />}`, or inside the attribute's object or array, `elementProps` too), each with the attribute it is in. Not its children, and not the elements inside those elements. */
92
+ /** The elements this one holds in its attributes (`content={<X />}`, or inside the attribute's object or array, `elementProps` too), each with the attribute it is in, its name, and its path: the name, then a step per object key down to the element; an array item's `_id` goes on the step of the key that holds the array. Not its children, and not the elements inside those elements.
93
+ * path is null when the way has no such spelling: a computed key, an array item that isn't an object with a string `_id`, an element directly in an array. */
94
94
  export function getAttributeElements(owner) {
95
- const elementsIn = (value) => {
95
+ const elementsIn = (written, path) => {
96
+ const value = written && bare(written);
96
97
  switch (value?.type) {
97
- case 'JSXElement': return [value];
98
- case 'JSXExpressionContainer': return elementsIn(value.expression);
99
- case 'ObjectExpression': return value.properties.flatMap((property) => (property.type === 'Property' ? elementsIn(property.value) : []));
100
- case 'ArrayExpression': return value.elements.flatMap(elementsIn);
98
+ case 'JSXElement': return [{ element: value, path }];
99
+ case 'JSXExpressionContainer': return elementsIn(value.expression, path);
100
+ case 'ObjectExpression': return value.properties.flatMap((property) => {
101
+ const key = getKeyName(property);
102
+ return property.type === 'Property' ? elementsIn(property.value, path && key !== null ? [...path, { key }] : null) : [];
103
+ });
104
+ case 'ArrayExpression': return value.elements.flatMap((item) => {
105
+ const itemId = getItemId(item);
106
+ return elementsIn(item, path && itemId !== null ? [...path.slice(0, -1), { key: path.at(-1).key, itemId }] : null);
107
+ });
101
108
  default: return [];
102
109
  }
103
110
  };
104
- return owner.openingElement.attributes.filter(isAttribute).flatMap((attribute) => elementsIn(attribute.value).map((element) => ({ element, attribute: attribute })));
111
+ return owner.openingElement.attributes.filter(isAttribute).flatMap((attribute) => {
112
+ const name = getAttributeName(attribute);
113
+ return elementsIn(attribute.value, [{ key: name }]).map(({ element, path }) => ({ element, attribute: attribute, name, path }));
114
+ });
115
+ }
116
+ /** An attribute's value as plain data: literals (including a negative number such as `-1`), objects and arrays. undefined for what has no known value (no such attribute, an element, a variable, a call); inside an object or array such a value stays as undefined in its place. A computed key and a spread are left out. */
117
+ export function getAttributeValue(owner, name) {
118
+ const attribute = owner.openingElement.attributes.filter(isAttribute).find((candidate) => getAttributeName(candidate) === name);
119
+ const valueOf = (written) => {
120
+ const value = written && bare(written);
121
+ switch (value?.type) {
122
+ case 'Literal': return value.value instanceof RegExp ? undefined : typeof value.value === 'bigint' ? Number(value.value) : value.value;
123
+ case 'JSXExpressionContainer': return valueOf(value.expression);
124
+ case 'UnaryExpression': {
125
+ const argument = value.argument && bare(value.argument);
126
+ const number = value.operator === '-' && argument?.type === 'Literal' ? valueOf(argument) : undefined;
127
+ return typeof number === 'number' ? -number : undefined;
128
+ }
129
+ case 'ObjectExpression': return Object.fromEntries(value.properties.flatMap((property) => {
130
+ const key = getKeyName(property);
131
+ return property.type === 'Property' && key !== null ? [[key, valueOf(property.value)]] : [];
132
+ }));
133
+ case 'ArrayExpression': return value.elements.map(valueOf);
134
+ default: return undefined;
135
+ }
136
+ };
137
+ return valueOf(attribute?.value);
105
138
  }
139
+ /** `a`, `'a'` → 'a'; a computed key, or one that isn't a name or a string → null. */
140
+ const getKeyName = (property) => property.computed || !property.key ? null : property.key.type === 'Identifier' ? (property.key.name ?? null) : property.key.type === 'Literal' && typeof property.key.value === 'string' ? property.key.value : null;
141
+ /** The string `_id` of an array item that is an object with one (parentheses allowed around the item and the id); null for any other item. */
142
+ const getItemId = (written) => {
143
+ const item = written && bare(written);
144
+ if (item?.type !== 'ObjectExpression')
145
+ return null;
146
+ const writtenId = item.properties.find((property) => property.type === 'Property' && getKeyName(property) === '_id')?.value;
147
+ const id = writtenId && bare(writtenId);
148
+ return id?.type === 'Literal' && typeof id.value === 'string' ? id.value : null;
149
+ };
150
+ /** `content`, or `xlink:href` for a namespaced name. */
151
+ const getAttributeName = (attribute) => (attribute.name.type === 'JSXNamespacedName' ? `${attribute.name.namespace.name}:${attribute.name.name.name}` : attribute.name.name);
106
152
  /** Elements, text and {expressions}, in order. Whitespace and JSX comments are skipped. */
107
153
  export function getChildren(element) {
108
154
  return element.children.filter((child) => {
@@ -151,6 +197,17 @@ export function isStructure(node) {
151
197
  return declarator?.type === 'VariableDeclarator' && declarator.id?.type === 'Identifier' && declarator.id.name === STRUCTURE
152
198
  && declarator.parent?.parent?.type === 'ExportNamedDeclaration' && declarator.parent.parent.parent?.type === 'Program';
153
199
  }
200
+ /** The import reads the element: it is not inside a function other than Structure or a renderItem template, a call, a condition, or an {expression} child. */
201
+ export function isReadByImport(element) {
202
+ for (let up = element.parent; up.type !== 'JSXElement' && up.type !== 'Program'; up = up.parent) {
203
+ if (up.type === 'ArrowFunctionExpression')
204
+ return isStructure(up) || getCallbackKind(up) === 'template';
205
+ if (NOT_READ_IN.includes(up.type) || (up.type === 'JSXExpressionContainer' && up.parent.type !== 'JSXAttribute'))
206
+ return false;
207
+ }
208
+ return true;
209
+ }
210
+ const NOT_READ_IN = ['FunctionExpression', 'CallExpression', 'ConditionalExpression', 'LogicalExpression'];
154
211
  /** The node's parent, past parentheses, and its child on the way up: the node itself, or the parentheses around it. */
155
212
  function getOuter(node) {
156
213
  let child = node;
@@ -237,6 +294,7 @@ export function getPropNames(fn) {
237
294
  return null;
238
295
  return (param.properties ?? []).map(({ type, computed, key, value }) => type === 'Property' && !computed && key?.type === 'Identifier' && value?.type === 'Identifier' && value.name === key.name ? String(key.name) : null);
239
296
  }
297
+ export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
240
298
  const isImport = (statement) => statement.type === 'ImportDeclaration';
241
299
  const isAttribute = (attribute) => attribute.type === 'JSXAttribute';
242
300
  const isIdAttribute = (attribute) => isAttribute(attribute) && attribute.name.type === 'JSXIdentifier' && attribute.name.name === 'id';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.22",
3
+ "version": "1.34.24",
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": {
@@ -25,7 +25,7 @@
25
25
  "oxc-parser": "^0.151.0",
26
26
  "oxlint": "^1.39.0",
27
27
  "postcss": "^8.5.3",
28
- "@wix/origami-bi": "0.11.0"
28
+ "@wix/origami-bi": "0.12.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.0.0",
@@ -52,5 +52,5 @@
52
52
  ]
53
53
  }
54
54
  },
55
- "falconPackageHash": "1837a281f36b5802f5c7a2d6ebb74b8c10661be0bc7a58f66c923836"
55
+ "falconPackageHash": "1bb53e289c65cf4acdcbc54a85fcf823b282d0e1be7bac656dff5170"
56
56
  }