@wix/origami-lint 1.34.32 → 1.34.33
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/dist/rules/index.js +2 -1
- package/dist/rules/site-app/css-layout-properties.js +9 -15
- package/dist/rules/site-app/layout-design-notes.js +277 -0
- package/dist/rules-api/index.js +2 -1
- package/dist/rules-api/rules-api.js +8 -2
- package/dist/rules-api/utils.js +2 -0
- package/dist/shared/css.js +5 -0
- package/dist/shared/utils.js +4 -0
- package/package.json +2 -2
package/dist/rules/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import cssSelectors from "./site-app/css-selectors.js";
|
|
|
8
8
|
import dataValues from "./site-app/data-values.js";
|
|
9
9
|
import globalStyles from "./site-app/global-styles.js";
|
|
10
10
|
import knownTags from "./site-app/known-tags.js";
|
|
11
|
+
import layoutDesignNotes from "./site-app/layout-design-notes.js";
|
|
11
12
|
import noAbsoluteSection from "./site-app/no-absolute-section.js";
|
|
12
13
|
import pageStructure from "./site-app/page-structure.js";
|
|
13
14
|
import slotAttributes from "./site-app/slot-attributes.js";
|
|
@@ -17,5 +18,5 @@ import themeFontKnown from "./site-app/theme-font-known.js";
|
|
|
17
18
|
import themeRoot from "./site-app/theme-root.js";
|
|
18
19
|
import noFreeCode from "./site-app/no-free-code.js";
|
|
19
20
|
// Execution registries combine already-checked rules; only this boundary erases their individual key unions.
|
|
20
|
-
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags];
|
|
21
|
+
export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags, layoutDesignNotes];
|
|
21
22
|
export { defineRule } from "./site-app/define-rule.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTENT_TARGET, createCssFileReporter,
|
|
1
|
+
import { CONTENT_TARGET, createCssFileReporter, getLayoutLiteral, OVERFLOW_TARGET } from "../../rules-api/index.js";
|
|
2
2
|
import { defineRule } from "./define-rule.js";
|
|
3
3
|
const CHECKS = ['duplicate_property', 'non_layout_property', 'property_outside_target', 'property_in_wrong_target', 'non_container_target_property', 'missing_property_value', 'incompatible_container_property', 'invalid_layout_value', 'missing_container_display', 'missing_grid_column', 'invalid_spx_context', 'unsupported_flex_wrap', 'child_without_container_layout'];
|
|
4
4
|
// ── the tables (propSpecs.ts) ──
|
|
@@ -248,13 +248,10 @@ function checkItemParents(input) {
|
|
|
248
248
|
for (const child of getItems(input))
|
|
249
249
|
checkContainerParent(child, parent, input);
|
|
250
250
|
}
|
|
251
|
-
/** The
|
|
252
|
-
|
|
253
|
-
const held = rulesApi.getAttributeElements(node).filter(({ attribute }) => !LAYER_ATTRIBUTES.includes(getName(attribute))).map(({ element }) => element);
|
|
254
|
-
return [...rulesApi.getChildElements(node), ...held].flatMap((element) => rulesApi.getComponent(element) ?? []);
|
|
255
|
-
}
|
|
251
|
+
/** The components this element lays out (its items, as the import reads structure.jsx). */
|
|
252
|
+
const getItems = ({ node, rulesApi }) => rulesApi.getItemElements(node).flatMap((element) => rulesApi.getComponent(element) ?? []);
|
|
256
253
|
/** Reports each breakpoint at which the child sets item properties in layout.css while its parent has no `display: grid` or `flex` there or by default; not for a `position: fixed` child (its last position at that breakpoint). */
|
|
257
|
-
function checkContainerParent(child, parent, { report }) {
|
|
254
|
+
function checkContainerParent(child, parent, { report, rulesApi }) {
|
|
258
255
|
const byMedia = new Map();
|
|
259
256
|
for (const prop of Object.keys(ITEM_PROPS)) {
|
|
260
257
|
for (const decl of (child.css.layout[prop] ?? []).filter(isOnElement))
|
|
@@ -263,7 +260,7 @@ function checkContainerParent(child, parent, { report }) {
|
|
|
263
260
|
for (const [media, declarations] of byMedia) {
|
|
264
261
|
if (declarations.filter(({ prop }) => prop === 'position').at(-1)?.decl.value.trim() === 'fixed')
|
|
265
262
|
continue; // the last position wins, as the import merges the scope
|
|
266
|
-
if (isContainerAt(parent, media))
|
|
263
|
+
if (isContainerAt(parent, media, rulesApi))
|
|
267
264
|
continue;
|
|
268
265
|
const childName = nameOf(child);
|
|
269
266
|
const parentName = nameOf(parent);
|
|
@@ -272,10 +269,9 @@ function checkContainerParent(child, parent, { report }) {
|
|
|
272
269
|
report({ check: 'child_without_container_layout', loc: child.loc, message: `layout.css:${line}: ${childName} sets item properties (${props}), but its parent ${parentName} has no container layout${media ? ` at @media ${media}` : ''}: give ${parentName} \`display: grid\` or \`display: flex\`, or remove ${childName}'s item properties.` });
|
|
273
270
|
}
|
|
274
271
|
}
|
|
275
|
-
/** The parent's container type at this breakpoint, as the import derives it: its
|
|
276
|
-
function isContainerAt(parent, media) {
|
|
277
|
-
const
|
|
278
|
-
const isContainer = (at) => ['grid', 'flex'].includes(displays.filter((decl) => decl.media === at).at(-1)?.value.trim() ?? '');
|
|
272
|
+
/** The parent's container type at this breakpoint, as the import derives it: its `display` there (on itself or in its target blocks) when it is grid or flex, else the default's. */
|
|
273
|
+
function isContainerAt(parent, media, rulesApi) {
|
|
274
|
+
const isContainer = (at) => ['grid', 'flex'].includes(rulesApi.getLayoutCss(parent, 'display', at)?.value.trim() ?? '');
|
|
279
275
|
return isContainer(media) || (media !== null && isContainer(null));
|
|
280
276
|
}
|
|
281
277
|
/** On the element itself, not in a target block or on an inner part; at any breakpoint. */
|
|
@@ -335,13 +331,11 @@ function mergeScopes(blocks) {
|
|
|
335
331
|
// ── reading values (valueDeserializers.ts) ──
|
|
336
332
|
/** A value reads as the import reads it: a site layout variable's token, `var(--name, <fallback>)`, by its fallback (layoutVariables.ts literalOf); `--spx(12)` as `12spx`. */
|
|
337
333
|
function isReadable(spec, value) {
|
|
338
|
-
const literal =
|
|
334
|
+
const literal = getLayoutLiteral(value);
|
|
339
335
|
if (spec.values)
|
|
340
336
|
return spec.values.includes(literal);
|
|
341
337
|
return spec.reads ? spec.reads(restoreSpx(literal)) : true;
|
|
342
338
|
}
|
|
343
|
-
/** `var(--name, <fallback>)`: a field connected to one of the site's layout variables, written with the field's own value as the fallback. */
|
|
344
|
-
const LAYOUT_VARIABLE_TOKEN = /^var\(--[A-Za-z_-][\w-]*,\s*(.+)\)$/;
|
|
345
339
|
/** The import reads `--spx(12)` as `12spx` before it reads a value (spxTransformer.ts restoreSpxValue). */
|
|
346
340
|
const restoreSpx = (value) => value.replace(/--spx\((-?\d+(?:\.\d+)?)\)/g, '$1spx');
|
|
347
341
|
const SIZE_KEYWORDS = ['auto', 'fit-content', 'max-content', 'min-content', 'unset'];
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// The layout-design-notes subject (tracker tab css-subjects, rows KIT-*): layout choices the editor's later editing depends on, as the kit's
|
|
2
|
+
// notes check them (origami PR #45 at 01510ad, validate.mjs: stackPolicyNotes :250-283, oneColumnGrids :197-223, boxIssues :157-169, noMobile
|
|
3
|
+
// :446, :556). Warnings, never errors: the import accepts every case; the design call stays the author's. In a page's or global component's
|
|
4
|
+
// layout.css, read with its structure.jsx:
|
|
5
|
+
// - a Section, HeaderSection or FooterSection and its canvas (its InternalContainer) are a grid, never `display: flex` (KIT-sectionStack);
|
|
6
|
+
// - a BoxContainer carries no `display: flex`, the stack goes on its InternalContainer (KIT-boxStack), which has no width or height of its
|
|
7
|
+
// own (KIT-icSize); the box of a row stack is `width: max-content`, not a % (KIT-rowStackBox);
|
|
8
|
+
// - an InternalContainer that isn't a canvas and isn't inside a Menu: a one-column grid with two or more children in separate rows is a
|
|
9
|
+
// Stack (KIT-oneColumnGrid, by default only); a stack with one child is a grid (KIT-stackSingle); a stack's children each have their own
|
|
10
|
+
// order, 1 to n (KIT-stackOrder); by default, the orders follow the JSX order (KIT-stackOrderJsx);
|
|
11
|
+
// - when the site or the page has a breakpoint, a component with no rule for it reuses its desktop layout (KIT-noMobile): one line per
|
|
12
|
+
// page and breakpoint, from the page's root.
|
|
13
|
+
// A breakpoint is its `@media` query as written; a breakpoint without a value takes the default's, as the import merges the element's
|
|
14
|
+
// scope (rulesApi.getLayoutCss). A breakpoint's note that repeats the default's (inherited, not restated) is reported once.
|
|
15
|
+
import { FILE_START } from "../../page-model/index.js";
|
|
16
|
+
import { getLayoutLiteral, GLOBAL_SECTIONS, isElementDeclaration, SECTIONS, WRAPPERS } from "../../rules-api/index.js";
|
|
17
|
+
import { defineRule } from "./define-rule.js";
|
|
18
|
+
const CHECKS = ['section_stack', 'box_stack', 'inner_container_size', 'row_stack_box_width', 'one_column_grid', 'stack_single_child', 'stack_order', 'stack_order_jsx', 'no_mobile_rule'];
|
|
19
|
+
export default defineRule({
|
|
20
|
+
name: 'layout-design-notes',
|
|
21
|
+
editors: ['studio2'],
|
|
22
|
+
severity: 'warn',
|
|
23
|
+
checks: CHECKS,
|
|
24
|
+
callbacks: {
|
|
25
|
+
JSXElement: checkElementLayout,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
/** The notes on one element, by what it is: a section, a BoxContainer, an InternalContainer; the page's mobile rules from its root. */
|
|
29
|
+
function checkElementLayout(input) {
|
|
30
|
+
const { node, rulesApi } = input;
|
|
31
|
+
const component = rulesApi.getComponent(node);
|
|
32
|
+
if (!component)
|
|
33
|
+
return;
|
|
34
|
+
const type = rulesApi.getType(node);
|
|
35
|
+
if (type !== null && SECTIONS.includes(type))
|
|
36
|
+
checkSectionIsGrid(component, 'a section', input);
|
|
37
|
+
if (type === 'box')
|
|
38
|
+
checkBox(component, input);
|
|
39
|
+
if (type === 'innerElement')
|
|
40
|
+
checkInternalContainer(component, input);
|
|
41
|
+
if (rulesApi.getRoot() === component)
|
|
42
|
+
checkMobileRules(input);
|
|
43
|
+
}
|
|
44
|
+
// ── a section and its canvas (KIT-sectionStack) ──
|
|
45
|
+
/** A section, or a section's canvas, with `display: flex`. */
|
|
46
|
+
function checkSectionIsGrid(component, what, input) {
|
|
47
|
+
reportAtEachBreakpoint([component], (media) => {
|
|
48
|
+
const display = getLayoutLine(input.rulesApi, component, 'display', media);
|
|
49
|
+
if (!isStack(display))
|
|
50
|
+
return null;
|
|
51
|
+
return { loc: component.loc, line: display.line, text: `${nameOf(component)}, ${what}, has \`display: flex\`: ${what} is always a grid, write \`display: grid\``, row: 'KIT-sectionStack', check: 'section_stack' };
|
|
52
|
+
}, input);
|
|
53
|
+
}
|
|
54
|
+
// ── a BoxContainer and its InternalContainer (KIT-boxStack, KIT-icSize, KIT-rowStackBox) ──
|
|
55
|
+
function checkBox(box, input) {
|
|
56
|
+
const { node, rulesApi } = input;
|
|
57
|
+
const innerElement = rulesApi.getItemElements(node).find((item) => rulesApi.getType(item) === 'innerElement'); // a child, or `children={<InternalContainer …>}`
|
|
58
|
+
const inner = innerElement ? rulesApi.getComponent(innerElement) : null;
|
|
59
|
+
checkBoxStack(box, inner, input);
|
|
60
|
+
if (!inner)
|
|
61
|
+
return;
|
|
62
|
+
checkBoxSize(box, inner, input);
|
|
63
|
+
checkRowStackBoxWidth(box, inner, input);
|
|
64
|
+
}
|
|
65
|
+
/** `display: flex` on the BoxContainer itself. */
|
|
66
|
+
function checkBoxStack(box, inner, input) {
|
|
67
|
+
reportAtEachBreakpoint([box], (media) => {
|
|
68
|
+
const display = getLayoutLine(input.rulesApi, box, 'display', media);
|
|
69
|
+
if (!isStack(display))
|
|
70
|
+
return null;
|
|
71
|
+
return { loc: box.loc, line: display.line, text: `${nameOf(box)} (BoxContainer) has \`display: flex\`: put the stack on its InternalContainer${inner ? `, ${nameOf(inner)}` : ''}`, row: 'KIT-boxStack', check: 'box_stack' };
|
|
72
|
+
}, input);
|
|
73
|
+
}
|
|
74
|
+
/** A width or height of its own on the BoxContainer's InternalContainer: not `100%` or `auto`, which fill the box (the export writes `width: 100%; height: auto`). */
|
|
75
|
+
function checkBoxSize(box, inner, input) {
|
|
76
|
+
for (const property of ['width', 'height']) {
|
|
77
|
+
reportAtEachBreakpoint([inner], (media) => {
|
|
78
|
+
const size = getLayoutLine(input.rulesApi, inner, property, media);
|
|
79
|
+
if (!size || FILLS_BOX.includes(size.value))
|
|
80
|
+
return null;
|
|
81
|
+
return { loc: inner.loc, line: size.line, text: `${nameOf(inner)} has \`${property}: ${size.value}\`: put the size on its BoxContainer, ${nameOf(box)}`, row: 'KIT-icSize', check: 'inner_container_size' };
|
|
82
|
+
}, input);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** An InternalContainer size that fills its BoxContainer rather than sizing it. */
|
|
86
|
+
const FILLS_BOX = ['100%', 'auto'];
|
|
87
|
+
/** A % width on the BoxContainer of a row stack. */
|
|
88
|
+
function checkRowStackBoxWidth(box, inner, input) {
|
|
89
|
+
reportAtEachBreakpoint([box, inner], (media) => {
|
|
90
|
+
const { rulesApi } = input;
|
|
91
|
+
const width = getLayoutLine(rulesApi, box, 'width', media);
|
|
92
|
+
const direction = getLayoutLine(rulesApi, inner, 'flex-direction', media)?.value ?? 'row'; // CSS's default
|
|
93
|
+
if (!width || !width.value.endsWith('%') || !isStack(getLayoutLine(rulesApi, inner, 'display', media)) || !/^row(-reverse)?$/.test(direction))
|
|
94
|
+
return null;
|
|
95
|
+
return { loc: box.loc, line: width.line, text: `${nameOf(box)} has \`width: ${width.value}\`, but its InternalContainer ${nameOf(inner)} is a row stack: a row stack's box follows its content, write \`width: max-content\``, row: 'KIT-rowStackBox', check: 'row_stack_box_width' };
|
|
96
|
+
}, input);
|
|
97
|
+
}
|
|
98
|
+
// ── an InternalContainer's children (KIT-oneColumnGrid, KIT-stackSingle, KIT-stackOrder, KIT-stackOrderJsx) ──
|
|
99
|
+
function checkInternalContainer(inner, input) {
|
|
100
|
+
const holders = getHolders(input.node, input.rulesApi);
|
|
101
|
+
const holderType = holders[0] ? input.rulesApi.getType(holders[0]) : null;
|
|
102
|
+
if (holderType !== null && SECTIONS.includes(holderType))
|
|
103
|
+
return checkSectionIsGrid(inner, "a section's canvas", input);
|
|
104
|
+
if (holders.some((holder) => input.rulesApi.getType(holder) === 'menu'))
|
|
105
|
+
return;
|
|
106
|
+
const items = input.rulesApi.getItemElements(input.node).flatMap((element) => input.rulesApi.getComponent(element) ?? []);
|
|
107
|
+
checkOneColumnGrid(inner, items, input);
|
|
108
|
+
checkStackSingleChild(inner, items, input);
|
|
109
|
+
checkStackOrder(inner, items, input);
|
|
110
|
+
checkStackOrderJsx(inner, items, input);
|
|
111
|
+
}
|
|
112
|
+
/** By default: a one-column grid whose two or more children sit in separate rows. */
|
|
113
|
+
function checkOneColumnGrid(inner, items, { rulesApi, report }) {
|
|
114
|
+
const display = getLayoutLine(rulesApi, inner, 'display', null);
|
|
115
|
+
if (display?.value !== 'grid' || items.length < 2)
|
|
116
|
+
return;
|
|
117
|
+
const columns = (getLayoutLine(rulesApi, inner, 'grid-template-columns', null)?.value ?? '').replace(/^repeat\(\s*1\s*,\s*(.*)\)$/, '$1'); // repeat(1, <tracks>) is its tracks
|
|
118
|
+
if (columns.includes('repeat(') || columns.split(/\s+(?![^(]*\))/).length > 1)
|
|
119
|
+
return;
|
|
120
|
+
if (items.some((item) => !isFirstColumn(getLayoutLine(rulesApi, item, 'grid-column', null))))
|
|
121
|
+
return; // a child past the one track adds a column
|
|
122
|
+
const rows = items.map((item) => getRowRange(getLayoutLine(rulesApi, item, 'grid-row', null))).filter((range) => range !== null);
|
|
123
|
+
const layered = rows.some((a, index) => rows.slice(index + 1).some((b) => a[0] < b[1] && b[0] < a[1]));
|
|
124
|
+
if (layered)
|
|
125
|
+
return;
|
|
126
|
+
reportNote({ loc: inner.loc, line: display.line, text: `${nameOf(inner)} is a one-column grid with ${items.length} children in separate rows: a Stack (\`display: flex; flex-direction: column\`) keeps them editable as a group`, row: 'KIT-oneColumnGrid', check: 'one_column_grid' }, null, report);
|
|
127
|
+
}
|
|
128
|
+
/** Whether a `grid-column` keeps the child in the first column: none, `auto`, `1`, `span 1`, `1 / 2`, `1 / -1` or `1 / span 1`. */
|
|
129
|
+
const isFirstColumn = (gridColumn) => !gridColumn || /^(auto|span\s+1|1(\s*\/\s*(2|-1|span\s+1))?)$/.test(gridColumn.value);
|
|
130
|
+
/** A `grid-row` as the rows it occupies, [start, end): `2 / 4` → [2, 4], `2` → [2, 3]; a line counted from the end (`1 / -1`) as every row;
|
|
131
|
+
* null for anything else (a free row). */
|
|
132
|
+
function getRowRange(gridRow) {
|
|
133
|
+
const match = /^(-?\d+)(?:\s*\/\s*(-?\d+))?$/.exec(gridRow?.value ?? '');
|
|
134
|
+
if (!match)
|
|
135
|
+
return null;
|
|
136
|
+
// Without the row count, a line from the end can't be placed: taken as every row, so the grid isn't noted (read grid-template-rows to place it).
|
|
137
|
+
if (match[1].startsWith('-') || match[2]?.startsWith('-'))
|
|
138
|
+
return [-Infinity, Infinity];
|
|
139
|
+
const start = Number(match[1]);
|
|
140
|
+
const end = match[2] === undefined ? start + 1 : Number(match[2]);
|
|
141
|
+
return end > start ? [start, end] : null;
|
|
142
|
+
}
|
|
143
|
+
/** A stack with one child. */
|
|
144
|
+
function checkStackSingleChild(inner, items, input) {
|
|
145
|
+
if (items.length !== 1)
|
|
146
|
+
return;
|
|
147
|
+
reportAtEachBreakpoint([inner], (media) => {
|
|
148
|
+
const display = getLayoutLine(input.rulesApi, inner, 'display', media);
|
|
149
|
+
if (!isStack(display))
|
|
150
|
+
return null;
|
|
151
|
+
return { loc: inner.loc, line: display.line, text: `${nameOf(inner)} is a stack with one child, ${nameOf(items[0])}: a single child sits in a grid, write \`display: grid\``, row: 'KIT-stackSingle', check: 'stack_single_child' };
|
|
152
|
+
}, input);
|
|
153
|
+
}
|
|
154
|
+
/** A stack's children that share an order, or whose orders (all written) aren't 1 to n. A missing order isn't noted: the import fills the JSX order. */
|
|
155
|
+
function checkStackOrder(inner, items, input) {
|
|
156
|
+
if (items.length < 2)
|
|
157
|
+
return;
|
|
158
|
+
reportAtEachBreakpoint([inner, ...items], (media) => {
|
|
159
|
+
const { rulesApi } = input;
|
|
160
|
+
if (!isStack(getLayoutLine(rulesApi, inner, 'display', media)))
|
|
161
|
+
return null;
|
|
162
|
+
const orders = items.flatMap((item) => { const order = getLayoutLine(rulesApi, item, 'order', media); return order ? [{ item, order }] : []; });
|
|
163
|
+
const byValue = new Map();
|
|
164
|
+
for (const { item, order } of orders)
|
|
165
|
+
byValue.set(order.value, [...(byValue.get(order.value) ?? []), item]);
|
|
166
|
+
const shared = [...byValue].find(([, sharing]) => sharing.length > 1);
|
|
167
|
+
if (shared)
|
|
168
|
+
return { loc: inner.loc, line: orders[0].order.line, text: `stack children ${shared[1].map(nameOf).join(' and ')} share \`order: ${shared[0]}\`: give each child its own order, 1, 2, 3 …`, row: 'KIT-stackOrder', check: 'stack_order' };
|
|
169
|
+
if (orders.length !== items.length || isOneToN(orders.map(({ order }) => Number(order.value))))
|
|
170
|
+
return null;
|
|
171
|
+
return { loc: inner.loc, line: orders[0].order.line, text: `${nameOf(inner)}'s stack children use order ${orders.map(({ order }) => order.value).join(', ')}, not 1 to ${items.length}: give each child its own order, 1, 2, 3 …`, row: 'KIT-stackOrder', check: 'stack_order' };
|
|
172
|
+
}, input);
|
|
173
|
+
}
|
|
174
|
+
/** By default, on a stack that isn't reversed: orders (a clean 1 to n) that show the children in another order than the JSX. */
|
|
175
|
+
function checkStackOrderJsx(inner, items, { rulesApi, report }) {
|
|
176
|
+
if (items.length < 2 || !isStack(getLayoutLine(rulesApi, inner, 'display', null)))
|
|
177
|
+
return;
|
|
178
|
+
if (/reverse/.test(getLayoutLine(rulesApi, inner, 'flex-direction', null)?.value ?? ''))
|
|
179
|
+
return;
|
|
180
|
+
const orders = items.map((item) => getLayoutLine(rulesApi, item, 'order', null));
|
|
181
|
+
if (!orders.every((order) => order !== undefined) || !isOneToN(orders.map((order) => Number(order.value))))
|
|
182
|
+
return;
|
|
183
|
+
const shown = items.map((item, index) => ({ item, order: Number(orders[index].value) })).sort((a, b) => a.order - b.order).map(({ item }) => item);
|
|
184
|
+
if (shown.every((item, index) => item === items[index]))
|
|
185
|
+
return;
|
|
186
|
+
reportNote({ loc: inner.loc, line: orders[0].line, text: `${nameOf(inner)} shows its children as ${shown.map(nameOf).join(', ')}, unlike the JSX order ${items.map(nameOf).join(', ')} (fine when intended)`, row: 'KIT-stackOrderJsx', check: 'stack_order_jsx' }, null, report);
|
|
187
|
+
}
|
|
188
|
+
/** Whether these are 1, 2 … n in some order. */
|
|
189
|
+
const isOneToN = (numbers) => [...numbers].sort((a, b) => a - b).every((number, index) => number === index + 1);
|
|
190
|
+
// ── the page's mobile rules (KIT-noMobile) ──
|
|
191
|
+
/** At each breakpoint the site or the page has: the elements with a default layout rule but no rule for it; one line per breakpoint. Not the
|
|
192
|
+
* root, a reference to a global component (its page lays it out), a layer or a Background (it spans its owner at every breakpoint), or anything inside a Menu (the editor's own layout). */
|
|
193
|
+
function checkMobileRules({ node, rulesApi, report }) {
|
|
194
|
+
const laidOut = getLaidOutComponents(node, rulesApi);
|
|
195
|
+
const siteBreakpoints = rulesApi.getCustomMedia('theme').filter(({ query }) => query);
|
|
196
|
+
/** A breakpoint by its range: `(--tablet)` (or `( --tablet )`) as the theme declares it, `(max-width: 1023px)`; any other as written. */
|
|
197
|
+
const toRange = (media) => {
|
|
198
|
+
const name = media === null ? null : rulesApi.getBreakpointName(media);
|
|
199
|
+
return siteBreakpoints.find((breakpoint) => breakpoint.name === name)?.query ?? media;
|
|
200
|
+
};
|
|
201
|
+
// The page's breakpoints: from every component, the ones not counted too (a Background's `@media` is the page's).
|
|
202
|
+
const breakpoints = new Set([...siteBreakpoints.map(({ query }) => query), ...getBreakpoints(Object.values(rulesApi.page.components)).map(toRange)]);
|
|
203
|
+
for (const breakpoint of breakpoints) {
|
|
204
|
+
const missing = laidOut.filter((component) => hasLayoutAt(component, null) && !hasLayoutAt(component, breakpoint, toRange));
|
|
205
|
+
if (!missing.length)
|
|
206
|
+
continue;
|
|
207
|
+
const names = missing.slice(0, 6).map(nameOf).join(', ') + (missing.length > 6 ? ', …' : '');
|
|
208
|
+
report({ check: 'no_mobile_rule', loc: FILE_START, message: `layout.css: ${missing.length} element${missing.length === 1 ? ' has' : 's have'} no rule for @media ${breakpoint} and reuse${missing.length === 1 ? 's' : ''} the desktop layout as is (fine when intended): ${names} (tracker KIT-noMobile).` });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** The components the page lays out below its root: through every element and its items; not into a Menu, not a global component's reference or a Background. */
|
|
212
|
+
function getLaidOutComponents(root, rulesApi) {
|
|
213
|
+
const found = [];
|
|
214
|
+
const walk = (element) => {
|
|
215
|
+
for (const item of rulesApi.getItemElements(element)) {
|
|
216
|
+
const type = rulesApi.getType(item);
|
|
217
|
+
if (type !== null && (GLOBAL_SECTIONS.includes(type) || type === 'background'))
|
|
218
|
+
continue;
|
|
219
|
+
const component = rulesApi.getComponent(item);
|
|
220
|
+
if (component)
|
|
221
|
+
found.push(component);
|
|
222
|
+
if (type !== 'menu')
|
|
223
|
+
walk(item);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(root);
|
|
227
|
+
return found;
|
|
228
|
+
}
|
|
229
|
+
/** Whether the element's own layout lines (not a state's or an inner part's, as getLayoutCss reads them) include one at this breakpoint (null: by default), each line's `@media` read by `read`. */
|
|
230
|
+
const hasLayoutAt = (component, media, read = (written) => written) => Object.values(component.css.layout).some((lines) => lines.some((line) => read(line.media) === media && isElementDeclaration(line)));
|
|
231
|
+
// ── reporting ──
|
|
232
|
+
/** Runs a check by default, then at each `@media` these components' layout lines use; a breakpoint's note that repeats the default's isn't reported again. */
|
|
233
|
+
function reportAtEachBreakpoint(components, check, { report }) {
|
|
234
|
+
const byDefault = check(null);
|
|
235
|
+
if (byDefault)
|
|
236
|
+
reportNote(byDefault, null, report);
|
|
237
|
+
for (const media of getBreakpoints(components)) {
|
|
238
|
+
const note = check(media);
|
|
239
|
+
if (note && note.text !== byDefault?.text)
|
|
240
|
+
reportNote(note, media, report);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The `@media` queries these components' own layout lines use, as written, in order of first use. */
|
|
244
|
+
function getBreakpoints(components) {
|
|
245
|
+
const medias = new Set();
|
|
246
|
+
for (const component of components)
|
|
247
|
+
for (const lines of Object.values(component.css.layout))
|
|
248
|
+
for (const line of lines)
|
|
249
|
+
if (line.media !== null && isElementDeclaration(line))
|
|
250
|
+
medias.add(line.media);
|
|
251
|
+
return [...medias];
|
|
252
|
+
}
|
|
253
|
+
/** A note on its component: `layout.css:<line>: <text>[ at @media …] (tracker <row>).` */
|
|
254
|
+
function reportNote({ loc, line, text, row, check }, media, report) {
|
|
255
|
+
report({ check, loc, message: `layout.css:${line}: ${text}${media ? ` at @media ${media}` : ''} (tracker ${row}).` });
|
|
256
|
+
}
|
|
257
|
+
/** A layout line with its value as the import reads it: trimmed, a layout variable `var(--name, <fallback>)` by its fallback. */
|
|
258
|
+
function getLayoutLine(rulesApi, component, property, media) {
|
|
259
|
+
const line = rulesApi.getLayoutCss(component, property, media);
|
|
260
|
+
return line && { ...line, value: getLayoutLiteral(line.value.trim()) };
|
|
261
|
+
}
|
|
262
|
+
/** `display: flex`, the import's stack. */
|
|
263
|
+
const isStack = (display) => display?.value === 'flex';
|
|
264
|
+
/** The components around this element, closest first: wrappers are transparent. */
|
|
265
|
+
function getHolders(element, rulesApi) {
|
|
266
|
+
const holders = [];
|
|
267
|
+
for (let node = element.parent; node; node = node.parent) {
|
|
268
|
+
if (node.type !== 'JSXElement')
|
|
269
|
+
continue;
|
|
270
|
+
const type = rulesApi.getType(node);
|
|
271
|
+
if (type === null || !WRAPPERS.includes(type))
|
|
272
|
+
holders.push(node);
|
|
273
|
+
}
|
|
274
|
+
return holders;
|
|
275
|
+
}
|
|
276
|
+
/** How a message names the element: its first class as a selector, or its key when it has no class (and so no layout CSS). */
|
|
277
|
+
const nameOf = (component) => (component.classNames[0] ? `.${component.classNames[0]}` : component.key);
|
package/dist/rules-api/index.js
CHANGED
|
@@ -3,4 +3,5 @@ export { createRulesApi, getOrCreateRulesApi } from "./rules-api.js";
|
|
|
3
3
|
export { createCssFileReporter, describeAtRule, reportCssProblem } from "./report.js";
|
|
4
4
|
export { bare, getAttribute, getExpression, getKey, getName, getObject, getProperties, getCallbackKind, getParamReadKind, getProperty, isBound, isOneEditAway, isRecord, isReservedAttribute, isStructure, isReadByImport, ownerOf, } from "../shared/utils.js";
|
|
5
5
|
export { BUILDERS, CATALOG_FONTS, CALLBACK_PROP, DATA_KEY, ELEMENT_PROPS, EVENT_ATTRIBUTE, GLOBAL_SECTIONS, INSTANCE_PROPS, INTERACTIONS_PACKAGES, 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
|
-
export {
|
|
6
|
+
export { isElementDeclaration } from "./utils.js";
|
|
7
|
+
export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getLayoutLiteral, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
|
|
@@ -6,8 +6,8 @@ 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
8
|
import { getAttributeElements as getHeldElements, getAttributeValue, getChildren, getId, getImport, getImportKind, getImportedName, getPropNames, getSlot, getTagName, getType, hasDataAttributes, isTypeScript } from "../shared/utils.js";
|
|
9
|
-
import { readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
|
|
10
|
-
import { WRAPPERS } from "../shared/constants.js";
|
|
9
|
+
import { CONTENT_TARGET, readBreakpointName, readCustomMedia, readThemeFontTokens, readThemeTokens } from "../shared/css.js";
|
|
10
|
+
import { LAYER_ATTRIBUTES, WRAPPERS } from "../shared/constants.js";
|
|
11
11
|
import { getEnclosingAtRules, isBaseDeclaration } from "./utils.js";
|
|
12
12
|
/** Theme lines come from @apply inside style.css, so style.css's own lines usually come after them. */
|
|
13
13
|
const CASCADE_ORDER = ['layout', 'theme', 'style'];
|
|
@@ -26,6 +26,10 @@ export function createRulesApi(page) {
|
|
|
26
26
|
return isWrapper(child) ? getChildElements(child) : [child];
|
|
27
27
|
});
|
|
28
28
|
const getAttributeElements = (node) => getHeldElements(node).flatMap((held) => (isWrapper(held.element) ? getChildElements(held.element).map((inner) => ({ ...held, element: inner })) : [held]));
|
|
29
|
+
const getLayoutCss = (component, property, media) => {
|
|
30
|
+
const lines = (component.css.layout[property] ?? []).filter((line) => line.selector === '&' || line.selector === `& @origami-target ${CONTENT_TARGET}`);
|
|
31
|
+
return lines.filter((line) => line.media === media).at(-1) ?? (media === null ? undefined : lines.filter((line) => line.media === null).at(-1));
|
|
32
|
+
};
|
|
29
33
|
return {
|
|
30
34
|
page,
|
|
31
35
|
// A JSX element starts where its component starts.
|
|
@@ -34,6 +38,7 @@ export function createRulesApi(page) {
|
|
|
34
38
|
getRoot: () => Object.values(page.components).find((component) => !getParent(component)) ?? null,
|
|
35
39
|
getCss,
|
|
36
40
|
getBaseCss: (component, property) => getCss(component, property).filter(isBaseDeclaration).at(-1),
|
|
41
|
+
getLayoutCss,
|
|
37
42
|
getThemeTokens: () => readThemeTokens(page.css.files.theme),
|
|
38
43
|
getThemeFontTokens: () => readThemeFontTokens(page.css.files.theme),
|
|
39
44
|
getCustomMedia: (kind) => readCustomMedia(page.css.files[kind]),
|
|
@@ -46,6 +51,7 @@ export function createRulesApi(page) {
|
|
|
46
51
|
getChildren,
|
|
47
52
|
getChildElements,
|
|
48
53
|
getAttributeElements,
|
|
54
|
+
getItemElements: (node) => [...getChildElements(node), ...getAttributeElements(node).filter(({ name }) => !LAYER_ATTRIBUTES.includes(name)).map(({ element }) => element)],
|
|
49
55
|
getAttributeValue,
|
|
50
56
|
getSlot,
|
|
51
57
|
getImport: (node) => getImport(node.type === 'JSXElement' ? getTagName(node).split('.')[0] : node.name, getProgram(node)),
|
package/dist/rules-api/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** On the component itself, outside any @media and @origami-target: applies by default. */
|
|
2
2
|
export const isBaseDeclaration = (decl) => !decl.media && decl.selector === '&';
|
|
3
|
+
/** On the component itself (`&`) or in one of its `@origami-target` blocks, at any breakpoint: the lines the import merges into the element's layout scope; not a state's or an inner part's. */
|
|
4
|
+
export const isElementDeclaration = (decl) => decl.selector === '&' || decl.selector.startsWith('& @origami-target ');
|
|
3
5
|
/** The at-rules around a CSS node, outer to inner (`@media`, `@scope`, `@layer`, `@mixin`…); [] at the file's top level. */
|
|
4
6
|
export function getEnclosingAtRules(node) {
|
|
5
7
|
const atRules = [];
|
package/dist/shared/css.js
CHANGED
|
@@ -52,6 +52,11 @@ function parseCustomMedia(params) {
|
|
|
52
52
|
const [, name = '', query = ''] = /^(\S*)\s*([\s\S]*)$/.exec(params.trim()) ?? [];
|
|
53
53
|
return { name, query };
|
|
54
54
|
}
|
|
55
|
+
/** A layout value as the import reads it (layoutVariables.ts literalOf): `var(--name, <fallback>)`, a field connected to one of the site's layout
|
|
56
|
+
* variables, by its fallback; any other value as is. */
|
|
57
|
+
export const getLayoutLiteral = (value) => LAYOUT_VARIABLE_TOKEN.exec(value)?.[1] ?? value;
|
|
58
|
+
/** `var(--name, <fallback>)`: a field connected to one of the site's layout variables, written with the field's own value as the fallback. */
|
|
59
|
+
const LAYOUT_VARIABLE_TOKEN = /^var\(--[A-Za-z_-][\w-]*,\s*(.+)\)$/;
|
|
55
60
|
/** A `font` shorthand's weight and family; undefined for a `var(…)` or a value with no family after its size. */
|
|
56
61
|
export function readFontShorthand(value) {
|
|
57
62
|
if (value.includes('var('))
|
package/dist/shared/utils.js
CHANGED
|
@@ -9,6 +9,10 @@ const TYPE_BY_IMPORT_SOURCE = [
|
|
|
9
9
|
[new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?HeaderSection$`), 'header'],
|
|
10
10
|
[new RegExp(`^${SYSTEM_PACKAGE}/(?:layout/)?FooterSection$`), 'footer'],
|
|
11
11
|
[new RegExp(`^${SYSTEM_PACKAGE}/InternalContainer$`), 'innerElement'],
|
|
12
|
+
// The editor's containers and its background, as the export imports their wrappers.
|
|
13
|
+
[/^@origami-wrappers\/wixEditorElements\/boxcontainer$/, 'box'],
|
|
14
|
+
[/^@origami-wrappers\/wixEditorElements\/menu$/, 'menu'],
|
|
15
|
+
[/^@origami-wrappers\/wixSystemElements\/background$/, 'background'],
|
|
12
16
|
// A global component's folder is `<Type>_<id>`: HeaderSection_mb7ogqqn.
|
|
13
17
|
[new RegExp(`/${GLOBAL_COMPONENTS}/(?:HeaderSection|Header)_[^/]+$`), 'globalHeader'],
|
|
14
18
|
[new RegExp(`/${GLOBAL_COMPONENTS}/(?:FooterSection|Footer)_[^/]+$`), 'globalFooter'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/origami-lint",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.33",
|
|
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": "
|
|
55
|
+
"falconPackageHash": "fa4525b9f8f5fa63ad7d72b383deab33b1a9c16033290d6381eb0ee9"
|
|
56
56
|
}
|