@wix/origami-lint 1.34.29 → 1.34.31

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.
@@ -7,6 +7,7 @@ import cssLayoutProperties from "./site-app/css-layout-properties.js";
7
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
+ import knownTags from "./site-app/known-tags.js";
10
11
  import noAbsoluteSection from "./site-app/no-absolute-section.js";
11
12
  import pageStructure from "./site-app/page-structure.js";
12
13
  import slotAttributes from "./site-app/slot-attributes.js";
@@ -16,5 +17,5 @@ import themeFontKnown from "./site-app/theme-font-known.js";
16
17
  import themeRoot from "./site-app/theme-root.js";
17
18
  import noFreeCode from "./site-app/no-free-code.js";
18
19
  // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
19
- export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement];
20
+ export const ruleRegistry = [allowedImports, noFreeCode, pageStructure, noAbsoluteSection, themeFontIds, themeFontKnown, slotAttributes, themeRoot, breakpoints, cssFileNesting, globalStyles, applyAndPresets, bindings, dataValues, cssLayoutProperties, cssSelectors, slotPlacement, knownTags];
20
21
  export { defineRule } from "./site-app/define-rule.js";
@@ -1,4 +1,5 @@
1
1
  import homePageFolder from "./home-page-folder.js";
2
+ import themeConnectionNotes from "./theme-connection-notes.js";
2
3
  export { defineProjectRule } from "./define-project-rule.js";
3
4
  // Execution registries combine already-checked rules; only this boundary erases their individual key unions.
4
- export const projectRuleRegistry = [homePageFolder];
5
+ export const projectRuleRegistry = [homePageFolder, themeConnectionNotes];
@@ -0,0 +1,221 @@
1
+ // The theme-connection-notes subject: how an element's style.css connects to the Theme. Warnings: the import stores each value as
2
+ // written, so it takes all of these; the editor edits a half-connected element worse, and its Theme no longer changes a copied literal.
3
+ // - KIT-slotMix: an element takes every field from one slot of a family. A slot is the theme's --wst-* tokens of one look: a button
4
+ // (button-primary, -secondary, -tertiary), a box (box-primary, -secondary) or a type style (heading-1 to heading-6, paragraph-1 to -3).
5
+ // - KIT-slotPartial: an element connected to a slot writes no raw value on that slot's fields: a type slot's font or color, any color of
6
+ // a button or a box.
7
+ // - KIT-themeRepeat: a literal color or font written on three or more element rules across the site, when a --wst-* token in
8
+ // global/themes/theme.css holds it: connect them to the token.
9
+ // An element's fields are its classes' own rules in its page's, popup's or global component's style.css, joined as the cascade
10
+ // reads them (`className="base extra"` takes both): per breakpoint (`@media`) and property, the line that applies. Its inner parts
11
+ // (`.comp-a .item`) are left out, as the editor itself writes a slot's font with a raw color on them (converter fixture
12
+ // f16-c1dmp-real-page); so are states (`:hover`) and what an `@apply` puts in (a global style is the theme's). KIT-themeRepeat counts
13
+ // class rules, and needs every page at once, so this is a project rule: it builds each page's model and reads the theme.
14
+ import { buildPageModel, findPages, JSX_FILES } from "../../page-model/index.js";
15
+ import { readThemeTokens } from "../../shared/css.js";
16
+ import { POPUPS_FOLDER, SITE_SRC } from "../../shared/site-files-tree-mapping.js";
17
+ import { defineProjectRule } from "./define-project-rule.js";
18
+ /** One check per row: KIT-slotMix, KIT-slotPartial, KIT-themeRepeat. */
19
+ const CHECKS = ['mixed_slots', 'raw_slot_field', 'repeated_theme_literal'];
20
+ /** A var() of a slot's token: `var(--wst-heading-1-font)` → 'heading-1'. */
21
+ const SLOT_TOKEN = /var\(\s*--wst-(button-(?:primary|secondary|tertiary)|box-(?:primary|secondary)|heading-[1-6]|paragraph-[1-3])-/g;
22
+ /** A type slot's fields; a button's and a box's are every color they write. */
23
+ const TYPE_FIELDS = ['font', 'color'];
24
+ /** At least this many element rules write a literal before KIT-themeRepeat reports it. */
25
+ const REPEATS = 3;
26
+ /** At most this many rules are named in a KIT-themeRepeat message. */
27
+ const NAMED_RULES = 5;
28
+ /** A literal color in a value: `#` and 3 to 8 hex digits, or `rgb(…)` / `rgba(…)`. */
29
+ const LITERAL_COLOR = /#[0-9a-f]{3,8}\b|rgba?\([^)]*\)/gi;
30
+ const HEX_COLOR = /^#([0-9a-f]{3,8})$/i;
31
+ const RGB_COLOR = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+%?)\s*)?\)$/i;
32
+ const THEME_TOKEN = /^--wst-/;
33
+ const FONT_TOKEN = /^--wst-[\w-]+-font$/;
34
+ export default defineProjectRule({
35
+ name: 'theme-connection-notes',
36
+ checks: CHECKS,
37
+ editors: ['studio2'],
38
+ severity: 'warn',
39
+ check: (project) => {
40
+ const pages = getPageModels(project);
41
+ const holders = getThemeHolders(pages[0]);
42
+ return [
43
+ ...pages.flatMap(getElementsFields).flatMap(checkElementSlots),
44
+ ...[...getLiteralUses(pages.flatMap(getClassRulesFields))].flatMap(([key, uses]) => checkLiteralConnected(key, uses, holders)),
45
+ ];
46
+ },
47
+ });
48
+ // ── the steps ──
49
+ /** Per family of slots the element uses: one slot (KIT-slotMix), then that slot's fields connected (KIT-slotPartial). */
50
+ function checkElementSlots(element) {
51
+ return [...getSlotsByFamily(element)].flatMap(([family, slots]) => {
52
+ const mix = checkElementOneSlot(element, slots);
53
+ return mix.length ? mix : checkSlotFieldsConnected(element, family, slots[0].slot);
54
+ });
55
+ }
56
+ /** Reports an element that takes fields from two slots of one family, on the line the second is first used (KIT-slotMix). */
57
+ function checkElementOneSlot({ file, name }, slots) {
58
+ if (slots.length === 1)
59
+ return [];
60
+ const names = slots.map(({ slot }) => `--wst-${slot}-*`).join(' with ');
61
+ return [{ check: 'mixed_slots', file, line: slots[1].line, message: `${name} mixes ${names}. Connect every field to one slot (rule KIT-slotMix).` }];
62
+ }
63
+ /** Reports each raw value on the slot's fields: a type slot's font or color, any color of a button or a box (KIT-slotPartial). A transparent
64
+ * color is no raw look. */
65
+ function checkSlotFieldsConnected({ file, name, fields }, family, slot) {
66
+ return fields.flatMap(({ property, value, line }) => {
67
+ if (value.includes('var(') || !(family === 'type' ? TYPE_FIELDS.includes(property) && !isTransparent(value) : readLiteralColors(value).length > 0))
68
+ return [];
69
+ const token = family === 'type' ? `var(--wst-${slot}-${property})` : `a --wst-${slot}-* token with var()`;
70
+ return [{ check: 'raw_slot_field', file, line, message: `${name} is connected to --wst-${slot}-* but its ${property} is a raw value (${value}). Connect it to ${token}, or keep it as a deliberate one-off (rule KIT-slotPartial).` }];
71
+ });
72
+ }
73
+ /** Reports a literal written on three or more element rules that a theme token holds, on the first rule's line (KIT-themeRepeat). */
74
+ function checkLiteralConnected(key, uses, holders) {
75
+ const tokens = holders.get(key);
76
+ if (uses.length < REPEATS || tokens === undefined)
77
+ return [];
78
+ const [kind, value] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
79
+ const named = uses.slice(0, NAMED_RULES).map(({ file, name, line }) => `${name} in ${file.slice(SITE_SRC.length + 1)}:${line}`);
80
+ const rules = `${named.join(', ')}${uses.length > NAMED_RULES ? ', …' : ''}`;
81
+ const message = `${kind} ${value} is written on ${uses.length} rules (${rules}); ${tokens.join(' / ')} in theme.css holds it. Connect them with var(${tokens[0]}) (rule KIT-themeRepeat).`;
82
+ return [{ check: 'repeated_theme_literal', file: uses[0].file, line: uses[0].line, message }];
83
+ }
84
+ // ── reading the site ──
85
+ /** The fields of each element of a page: its classes' CSS joined (`component.css`, as the cascade reads them, in file order). Elements with
86
+ * the same set of classes, in any order, are one, named by the set sorted: `.base.extra`. */
87
+ function getElementsFields(page) {
88
+ const byName = new Map();
89
+ for (const component of Object.values(page.components)) {
90
+ const classNames = [...new Set(component.classNames.filter((className) => page.css.byClassName[className]))].sort();
91
+ if (classNames.length)
92
+ byName.set(`.${classNames.join('.')}`, component.css);
93
+ }
94
+ return [...byName].flatMap(([name, css]) => getFieldsByMedia(`${page.path}/style.css`, name, css));
95
+ }
96
+ /** Each class rule's fields: what KIT-themeRepeat counts, a rule written once whatever elements take it. */
97
+ function getClassRulesFields(page) {
98
+ return Object.entries(page.css.byClassName).flatMap(([className, css]) => getFieldsByMedia(`${page.path}/style.css`, `.${className}`, css));
99
+ }
100
+ /** One entry per breakpoint (`@media`): the own style.css lines (`&`: no part, no state), per property the last one, which applies. */
101
+ function getFieldsByMedia(file, name, css) {
102
+ const byMedia = new Map();
103
+ for (const [property, lines] of Object.entries(css.style)) {
104
+ for (const line of lines.filter(({ selector }) => selector === '&')) {
105
+ if (!byMedia.has(line.media))
106
+ byMedia.set(line.media, new Map());
107
+ byMedia.get(line.media).set(property, { property, ...line });
108
+ }
109
+ }
110
+ return [...byMedia.values()].map((fields) => ({ file, name, fields: [...fields.values()].sort((a, b) => a.line - b.line || a.column - b.column) }));
111
+ }
112
+ /** The model of each page, popup and global component, sorted by path; a page whose files can't be parsed is left out (the page rules report it,
113
+ * and a note never fails the run). */
114
+ function getPageModels({ files, readFile }) {
115
+ return [...findPages([...files], [...files]), ...getPopupJsxFiles(files)].sort().flatMap((jsxFile) => {
116
+ try {
117
+ return [buildPageModel({ jsxFile, text: readFile(jsxFile) ?? '' }, readFile)];
118
+ }
119
+ catch {
120
+ return [];
121
+ }
122
+ });
123
+ }
124
+ /** Each popup's structure file: one popup per folder directly under popups/, written like a page (structure.tsx wins over structure.jsx).
125
+ * findPages, which picks the page rules' pages, has no popups. */
126
+ function getPopupJsxFiles(files) {
127
+ const existing = new Set(files);
128
+ const folders = new Set(files.filter((file) => file.startsWith(`${POPUPS_FOLDER}/`) && file.split('/').length === POPUPS_FOLDER.split('/').length + 2).map((file) => file.slice(0, file.lastIndexOf('/'))));
129
+ return [...folders].flatMap((folder) => JSX_FILES.map((name) => `${folder}/${name}`).find((file) => existing.has(file)) ?? []);
130
+ }
131
+ /** The slots an element's fields use, by family, each once, in the order first used. */
132
+ function getSlotsByFamily({ fields }) {
133
+ const slots = new Map();
134
+ for (const { value, line } of fields) {
135
+ for (const [, slot] of value.matchAll(SLOT_TOKEN)) {
136
+ const family = getFamily(slot);
137
+ const uses = slots.get(family) ?? [];
138
+ if (!uses.some((use) => use.slot === slot))
139
+ slots.set(family, [...uses, { slot, line }]);
140
+ }
141
+ }
142
+ return slots;
143
+ }
144
+ /** 'button-primary' → 'button'; 'box-secondary' → 'box'; 'heading-1', 'paragraph-2' → 'type'. */
145
+ function getFamily(slot) {
146
+ if (slot.startsWith('button-'))
147
+ return 'button';
148
+ return slot.startsWith('box-') ? 'box' : 'type';
149
+ }
150
+ /** Each literal color and font the class rules write, with the rules that write it, each rule once, in order. */
151
+ function getLiteralUses(rules) {
152
+ const uses = new Map();
153
+ const add = (key, use) => {
154
+ const keyUses = uses.get(key) ?? [];
155
+ if (!keyUses.some(({ file, name }) => file === use.file && name === use.name))
156
+ uses.set(key, [...keyUses, use]);
157
+ };
158
+ for (const { file, name, fields } of rules) {
159
+ for (const { property, value, line } of fields) {
160
+ const literal = value.replace(/!important\s*$/i, '').trim();
161
+ if (literal.includes('var('))
162
+ continue;
163
+ if (property === 'font')
164
+ add(fontKey(literal), { file, name, line });
165
+ for (const color of readLiteralColors(literal))
166
+ add(`color:${color}`, { file, name, line });
167
+ }
168
+ }
169
+ return uses;
170
+ }
171
+ /** The theme's --wst-* tokens whose value is one literal color or a literal font, by that literal. Every page holds the same theme. */
172
+ function getThemeHolders(page) {
173
+ const holders = new Map();
174
+ for (const { name, value } of readThemeTokens(page?.css.files.theme)) {
175
+ if (!THEME_TOKEN.test(name) || value.includes('var('))
176
+ continue;
177
+ const color = normalizeColor(value);
178
+ const key = FONT_TOKEN.test(name) ? fontKey(value) : color && `color:${color}`;
179
+ if (key && !holders.get(key)?.includes(name))
180
+ holders.set(key, [...(holders.get(key) ?? []), name]);
181
+ }
182
+ return holders;
183
+ }
184
+ /** A font shorthand in one spelling: spaces collapsed, lower case. */
185
+ const fontKey = (value) => `font:${value.replace(/\s+/g, ' ').trim().toLowerCase()}`;
186
+ /** A literal color read: its channels in one spelling ('#116DFF', '#16f', 'rgb(17, 102, 255)' → '#116dff', '#1166ff') and its alpha, 0 to 1.
187
+ * Null for anything else ('red', 'var(--x)', '#12345'). */
188
+ function parseColor(color) {
189
+ const hex = HEX_COLOR.exec(color.trim())?.[1].toLowerCase();
190
+ const rgb = hex ? null : RGB_COLOR.exec(color.trim());
191
+ let channels;
192
+ let alpha = 1;
193
+ if (hex) {
194
+ const digits = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join('') : hex;
195
+ if (digits.length !== 6 && digits.length !== 8)
196
+ return null;
197
+ channels = [0, 2, 4].map((at) => parseInt(digits.slice(at, at + 2), 16));
198
+ if (digits.length === 8)
199
+ alpha = parseInt(digits.slice(6), 16) / 255;
200
+ }
201
+ else if (rgb) {
202
+ channels = rgb.slice(1, 4).map(Number);
203
+ if (rgb[4] !== undefined)
204
+ alpha = rgb[4].endsWith('%') ? parseFloat(rgb[4]) / 100 : Number(rgb[4]);
205
+ }
206
+ else
207
+ return null;
208
+ return { spelled: `#${channels.map((channel) => Math.round(channel).toString(16).padStart(2, '0')).join('')}`, alpha };
209
+ }
210
+ /** One spelling of a literal color, so two spellings of one color compare equal; with an alpha, '#116dff/0.5'. Null for a transparent
211
+ * color (alpha 0: not a look) and for anything that isn't a literal color. */
212
+ function normalizeColor(color) {
213
+ const parsed = parseColor(color);
214
+ if (parsed === null || parsed.alpha === 0)
215
+ return null;
216
+ return parsed.alpha >= 1 ? parsed.spelled : `${parsed.spelled}/${Number(parsed.alpha.toFixed(2))}`;
217
+ }
218
+ /** `transparent`, or a literal color with alpha 0: no look of its own, so not a raw value. */
219
+ const isTransparent = (value) => /^transparent$/i.test(value.trim()) || parseColor(value)?.alpha === 0;
220
+ /** The literal colors written in a value, each in one spelling (normalizeColor), in order; a transparent one is left out. */
221
+ const readLiteralColors = (value) => [...value.matchAll(LITERAL_COLOR)].flatMap(([color]) => normalizeColor(color) ?? []);
@@ -3,12 +3,10 @@
3
3
  // refuses a tag from them, parseJsx.ts:471, identity.ts:80; a call is free code, and fn() / ctx() refuse them as a source).
4
4
  import path from 'node:path';
5
5
  import { CSS_FILES, JSX_FILES } from "../../page-model/index.js";
6
- import { BUILDERS, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE } from "../../rules-api/index.js";
6
+ import { BUILDERS, INTERACTIONS_PACKAGES, RUNTIME_ONLY_BUILDERS, RUNTIME_PACKAGE } from "../../rules-api/index.js";
7
7
  import { GLOBAL_COMPONENTS, GLOBAL_COMPONENTS_FOLDER, GLOBAL_FOLDER, SITE_SRC } from "../../shared/site-files-tree-mapping.js";
8
8
  import { defineRule } from "./define-rule.js";
9
9
  const CHECKS = ['disallowed_import_source', 'interaction_import', 'missing_global_import_name', 'invalid_global_import_name', 'default_builder_import', 'aliased_builder_import', 'wrong_builder_source'];
10
- /** The interactions packages, which the import reserves: structure.jsx imports nothing from them. Matched by source, not by name: `Interact` from @site/site-code is the site's own. */
11
- const INTERACTIONS_PACKAGES = ['interactReact', 'interact', 'motionPresets'];
12
10
  const SOURCE_HINT = `Only packages, files in ${path.posix.basename(GLOBAL_FOLDER)}/, and ${CSS_FILES.map((file) => `'./${file}'`).join(' or ')} can be.`;
13
11
  export default defineRule({
14
12
  name: 'allowed-imports',
@@ -0,0 +1,100 @@
1
+ // How a structure file writes its tags, as the converter reads them (tagOf and validTagOrIssue, parseJsx.ts, converter 0.88):
2
+ // - a tag is <Name> or <NS.Name>: no <svg:rect>, no <UI.Forms.Input> (tracker CONV-parseJsx.ts:401);
3
+ // - a lowercase tag is an HTML element, which the import refuses: <div> (CONV-parseJsx.ts:379). page-structure already reports
4
+ // one at the top of the file or directly in a section, so this rule doesn't. <NS.Name> needs `import * as NS` (379);
5
+ // - no component comes from a source that holds none: the runtime (except <Remove>), a menu, `document/…`, @wix/sdk (CONV-parseJsx.ts:351).
6
+ // The DM's own `document` holds the classic components (<ClassicSection>). allowed-imports reports @wix/interact and
7
+ // @wix/motion-presets on the import line;
8
+ // - a tag's name is capitalized (CONV-parseJsx.ts:359). It reads the local name: the export imports a context provider as
9
+ // `import { cartContext as CartContext }`, and only the site's providers tell it apart from `import { image as Image }`.
10
+ // A capitalized tag with no import, <NS.Name> with no NS, and a namespace written as <NS> are TypeScript's (#409): it reports a name not in scope.
11
+ import { INTERACTIONS_PACKAGES, SECTIONS } from "../../rules-api/index.js";
12
+ import { defineRule } from "./define-rule.js";
13
+ const CHECKS = ['unsupported_tag_name', 'html_tag', 'named_import_as_namespace', 'tag_from_reserved_source', 'lowercase_tag'];
14
+ /** The converter's TAG_PATTERN (identity.ts). */
15
+ const TAG_PATTERN = /^[A-Z][A-Za-z0-9_$]*$/;
16
+ /** The sources the converter reserves (isReservedRegistry, identity.ts) that allowed-imports doesn't already report. */
17
+ const RESERVED_SOURCES = ['runtime', 'menu', 'document', 'sdk'];
18
+ /** The one tag the runtime has: the <Remove> wrapper. */
19
+ const RUNTIME_TAG = 'Remove';
20
+ export default defineRule({
21
+ name: 'known-tags',
22
+ checks: CHECKS,
23
+ editors: ['studio2'],
24
+ callbacks: {
25
+ JSXElement: (input) => {
26
+ if (!input.rulesApi.getComponent(input.node))
27
+ return;
28
+ if (!checkTagNameShape(input))
29
+ return;
30
+ if (!checkTagNotHtml(input))
31
+ return;
32
+ if (!checkTagNamespaceImport(input))
33
+ return;
34
+ if (!checkTagSource(input))
35
+ return;
36
+ checkTagCapitalized(input);
37
+ },
38
+ },
39
+ });
40
+ // ── the steps: each reports its problem; one that returns false ends the steps for the tag, as the converter stops at its first ──
41
+ /** Reports <svg:rect> and <UI.Forms.Input> (CONV-parseJsx.ts:401). */
42
+ function checkTagNameShape({ node, rulesApi, report }) {
43
+ const { name } = node.openingElement;
44
+ if (name.type === 'JSXIdentifier' || (name.type === 'JSXMemberExpression' && name.object.type === 'JSXIdentifier'))
45
+ return true;
46
+ report({ check: 'unsupported_tag_name', loc: name.loc, message: `<${rulesApi.getTagName(node)}> is not a supported tag name: write <Name> or <NS.Name> (one namespace import, one dot).` });
47
+ return false;
48
+ }
49
+ /** Reports a lowercase tag with no import: an HTML element (CONV-parseJsx.ts:379), unless page-structure reports it already. */
50
+ function checkTagNotHtml({ node, rulesApi, report }) {
51
+ const { name } = node.openingElement;
52
+ if (name.type !== 'JSXIdentifier' || !/^[a-z]/.test(name.name) || rulesApi.getImport(node))
53
+ return true;
54
+ if (!isReportedByPageStructure(node, rulesApi)) {
55
+ report({ check: 'html_tag', loc: name.loc, message: `<${name.name}> is an HTML tag, which the import refuses: use an imported component (import { Name } from '@origami-wrappers/<app>/<name>', then <Name>).` });
56
+ }
57
+ return false;
58
+ }
59
+ /** Reports <NS.Name> whose NS is imported by name, not as `import * as NS` (CONV-parseJsx.ts:379). With no import at all, it's TypeScript's; from an interactions package, allowed-imports'. */
60
+ function checkTagNamespaceImport({ node, rulesApi, report }) {
61
+ const { name } = node.openingElement;
62
+ const found = rulesApi.getImport(node);
63
+ if (name.type !== 'JSXMemberExpression' || name.object.type !== 'JSXIdentifier' || !found || found.imported === '*' || (found.kind && INTERACTIONS_PACKAGES.includes(found.kind)))
64
+ return true;
65
+ const namespace = name.object.name;
66
+ report({ check: 'named_import_as_namespace', loc: name.loc, message: `<${namespace}.${name.property.name}> reads \`${namespace}\` as a namespace, but \`${namespace}\` is imported by name: import it as \`import * as ${namespace} from '${found.source}'\`, or import ${name.property.name} by name and write <${name.property.name}>.` });
67
+ return false;
68
+ }
69
+ /** Reports a tag imported from a source that holds no components (CONV-parseJsx.ts:351). <Remove> from the runtime is a wrapper. */
70
+ function checkTagSource({ node, rulesApi, report }) {
71
+ const found = rulesApi.getImport(node);
72
+ if (!found?.kind || !RESERVED_SOURCES.includes(found.kind) || (found.kind === 'runtime' && found.imported === RUNTIME_TAG))
73
+ return true;
74
+ const tag = rulesApi.getTagName(node);
75
+ const menuHint = found.kind === 'menu' ? `, and use the menu as a value (items: ${tag}.items)` : '';
76
+ report({ check: 'tag_from_reserved_source', loc: node.openingElement.name.loc, message: `<${tag}> is imported from '${found.source}', which holds no components (menus, runtime builders, document/…, @wix/sdk): import the component from its @origami-wrappers package${menuHint}.` });
77
+ return false;
78
+ }
79
+ /** Reports a tag whose name isn't capitalized (CONV-parseJsx.ts:359): its local name, and the name the converter reads (the imported name), both.
80
+ * A namespace as a tag is TypeScript's; a tag from an interactions package, allowed-imports'. */
81
+ function checkTagCapitalized({ node, rulesApi, report }) {
82
+ const { name } = node.openingElement;
83
+ const found = rulesApi.getImport(node);
84
+ if (name.type !== 'JSXIdentifier' || !found || found.imported === '*' || (found.kind && INTERACTIONS_PACKAGES.includes(found.kind)))
85
+ return;
86
+ if (TAG_PATTERN.test(name.name) || TAG_PATTERN.test(found.imported))
87
+ return;
88
+ report({ check: 'lowercase_tag', loc: name.loc, message: `<${name.name}> must be capitalized: import the component under its capitalized export name (import { Name } from '${found.source}') and write <Name>; a lowercase tag is an HTML element.` });
89
+ }
90
+ // ── reading the code ──
91
+ /** page-structure reports an element at the top of the file, and one directly in a section: its closest holder, wrappers skipped, is none or a section. */
92
+ function isReportedByPageStructure(node, rulesApi) {
93
+ for (let holder = node.parent; holder; holder = holder.parent) {
94
+ if (holder.type !== 'JSXElement' || rulesApi.isWrapper(holder))
95
+ continue;
96
+ const type = rulesApi.getType(holder);
97
+ return type !== null && SECTIONS.includes(type) && rulesApi.getChildElements(holder).includes(node);
98
+ }
99
+ return true;
100
+ }
@@ -2,5 +2,5 @@
2
2
  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
- export { BUILDERS, CATALOG_FONTS, 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";
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
6
  export { BREAKPOINT_AT_RULES, CONTENT_TARGET, getCssChildren, getDashedId, getMixinId, getSegmentClass, getSelectorSegments, isCssIdentifier, isPresetProperty, isSafeCssIdentifier, ORIGAMI_TARGETS, OVERFLOW_TARGET, PREAMBLE_LAYER, readFontShorthand, readSegment } from "../shared/css.js";
@@ -40,6 +40,7 @@ export function createRulesApi(page) {
40
40
  getBreakpointName: readBreakpointName,
41
41
  getEnclosingAtRules,
42
42
  getType: (node) => getType(node, getProgram(node)),
43
+ isWrapper,
43
44
  getTagName,
44
45
  getId,
45
46
  getChildren,
@@ -8,6 +8,8 @@ export const SECTIONS = ['section', 'header', 'footer'];
8
8
  export const GLOBAL_SECTIONS = ['globalHeader', 'globalFooter', 'globalSection'];
9
9
  /** A tag that wraps one component and isn't one itself: `<Remove when={…}>`, `<Interaction interactKey="…">`, `<PinnedToScreen>` (the import drops it). */
10
10
  export const WRAPPERS = ['remove', 'interaction', 'pinnedToScreen'];
11
+ /** The interactions packages, which the import reserves: structure.jsx imports nothing from them. Matched by source, not by name: `Interact` from @site/site-code is the site's own. */
12
+ export const INTERACTIONS_PACKAGES = ['interactReact', 'interact', 'motionPresets'];
11
13
  /** The name a structure file exports its tree under: `export const Structure = () => (…)`. */
12
14
  export const STRUCTURE = 'Structure';
13
15
  /** The props a global component's Structure may take: its instance identity, `({ id, className })` (#300). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/origami-lint",
3
- "version": "1.34.29",
3
+ "version": "1.34.31",
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": "2c36ab85acce27a2dc10d53da990328e863a2935e38f2d82a6dd7467"
55
+ "falconPackageHash": "686d35f346108816f50b0d95576cb023e273a903c797136d43187f3e"
56
56
  }