@wix/origami-lint 1.34.26 → 1.34.28

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.
@@ -37,7 +37,7 @@ function checkTopLevel(root, file, report) {
37
37
  }
38
38
  function checkTopAtRule(node, file, isLeadingImport, reportInFile) {
39
39
  if (node.name === 'import' && isLeadingImport)
40
- return reportInFile(node, 'css_import', `\`@import ${node.params.trim()}\` can't be in ${file}: an item's CSS carries no imports (the preview loads the theme from _generated/). Remove the line (tracker CONV-fromBundle.ts:987).`);
40
+ return reportInFile(node, 'css_import', `\`@import ${node.params.trim()}\` can't be in ${file}: an item's CSS carries no imports (the preview loads the theme from _generated/). Remove the line.`);
41
41
  if (node.name === 'custom-media')
42
42
  return;
43
43
  if (node.name === 'mixin' && file !== 'layout.css')
@@ -48,19 +48,19 @@ function checkTopAtRule(node, file, isLeadingImport, reportInFile) {
48
48
  return;
49
49
  if (BREAKPOINT_AT_RULES.includes(node.name))
50
50
  return checkBreakpoint(node, file, reportInFile);
51
- reportInFile(node, 'unsupported_top_level_at_rule', `\`@${node.name}\` isn't part of the export format at the top of ${file}. At the top level only @media, @scope, @custom-media${file === 'layout.css' ? '' : ' and @mixin'} are allowed${pageMixinNote(file)} (tracker CONV-utils.ts:92).`);
51
+ reportInFile(node, 'unsupported_top_level_at_rule', `\`@${node.name}\` isn't part of the export format at the top of ${file}. At the top level only @media, @scope, @custom-media${file === 'layout.css' ? '' : ' and @mixin'} are allowed${pageMixinNote(file)}.`);
52
52
  }
53
53
  /** Reports a bare declaration at the top of layout.css (CONV-astParser.ts:412); style.css and theme.css drop one silently. */
54
54
  function checkTopLevelDeclaration(node, file, reportInFile) {
55
55
  if (file !== 'layout.css')
56
56
  return;
57
- reportInFile(node, 'top_level_declaration', `\`${node.prop}: ${node.value}\` is at the top level. Put the declaration inside the element's rule (\`.comp-a { … }\`) (tracker CONV-astParser.ts:412).`);
57
+ reportInFile(node, 'top_level_declaration', `\`${node.prop}: ${node.value}\` is at the top level. Put the declaration inside the element's rule (\`.comp-a { … }\`).`);
58
58
  }
59
59
  /** A layout `@scope` holds exactly one `@media`, checked before its block (CONV-astParser.ts:399, as parseCssAst does). */
60
60
  function checkLayoutScope(node, reportInFile) {
61
61
  const scopeChildren = node.nodes === undefined ? [] : getCssChildren(node);
62
62
  if (scopeChildren.length !== 1 || !isMedia(scopeChildren[0])) {
63
- return reportInFile(node, 'invalid_scope_content', `\`@scope ${node.params.trim()}\` must hold exactly one \`@media (…) { … }\` block. Put the rules inside one @media in the @scope (tracker CONV-astParser.ts:399).`);
63
+ return reportInFile(node, 'invalid_scope_content', `\`@scope ${node.params.trim()}\` must hold exactly one \`@media (…) { … }\` block. Put the rules inside one @media in the @scope.`);
64
64
  }
65
65
  if (hasBlock(scopeChildren[0], reportInFile))
66
66
  checkBreakpointContent(scopeChildren[0], 'layout.css', reportInFile);
@@ -83,7 +83,7 @@ function getScopedMedia(node, reportInFile) {
83
83
  continue;
84
84
  }
85
85
  if (!isMedia(first)) {
86
- reportInFile(current, 'invalid_scope_content', `\`@scope ${current.params.trim()}\` must start with a \`@media (…) { … }\` block: put the rules inside it (tracker CONV-utils.ts:92).`);
86
+ reportInFile(current, 'invalid_scope_content', `\`@scope ${current.params.trim()}\` must start with a \`@media (…) { … }\` block: put the rules inside it.`);
87
87
  return null;
88
88
  }
89
89
  if (!hasBlock(first, reportInFile))
@@ -98,16 +98,16 @@ function checkBreakpointContent(media, file, reportInFile) {
98
98
  if (child.type === 'rule')
99
99
  checkRule(child, file, reportInFile);
100
100
  else if (child.type === 'atrule')
101
- reportInFile(child, 'at_rule_in_breakpoint', `\`@${child.name}\` inside \`${describeAtRule(media)}\` is not allowed. A breakpoint block holds only element rules: declare \`@mixin\` at the top level${pageMixinNote(file)} and put its breakpoint variant in a \`@media\` inside its \`@result\`; don't nest one \`@media\` in another (tracker CONV-astParser.ts:147).`);
101
+ reportInFile(child, 'at_rule_in_breakpoint', `\`@${child.name}\` inside \`${describeAtRule(media)}\` is not allowed. A breakpoint block holds only element rules: declare \`@mixin\` at the top level${pageMixinNote(file)} and put its breakpoint variant in a \`@media\` inside its \`@result\`; don't nest one \`@media\` in another.`);
102
102
  else if (child.type === 'decl')
103
- reportInFile(child, 'declaration_in_breakpoint', `\`${child.prop}: ${child.value}\` sits directly in \`${describeAtRule(media)}\`. Wrap it in the element's rule: \`@media (…) { .comp-a { … } }\` (tracker CONV-astParser.ts:151).`);
103
+ reportInFile(child, 'declaration_in_breakpoint', `\`${child.prop}: ${child.value}\` sits directly in \`${describeAtRule(media)}\`. Wrap it in the element's rule: \`@media (…) { .comp-a { … } }\`.`);
104
104
  }
105
105
  }
106
106
  /** Reports an at-rule written without a block (CONV-utils.ts:76): the import reads every top-level at-rule but `@custom-media` and `@mixin` as a breakpoint. */
107
107
  function hasBlock(node, reportInFile) {
108
108
  if (node.nodes !== undefined)
109
109
  return true;
110
- reportInFile(node, 'missing_at_rule_block', `\`${describeAtRule(node)}\` has no block. Give it a \`{ … }\` block with the element rules, or remove it (tracker CONV-utils.ts:76).`);
110
+ reportInFile(node, 'missing_at_rule_block', `\`${describeAtRule(node)}\` has no block. Give it a \`{ … }\` block with the element rules, or remove it.`);
111
111
  return false;
112
112
  }
113
113
  /** An element rule: layout's (CONV-astParser.ts:74, :51, :54, :57) or style's and theme's (CONV-astParser.ts:95). */
@@ -129,19 +129,19 @@ function checkLayoutRule(rule, reportInFile) {
129
129
  }
130
130
  const what = child.type === 'rule' ? `the rule \`${child.selector.trim()}\`` : `\`${describeAtRule(child)}\``;
131
131
  const fix = child.type === 'rule' ? 'Write it as its own rule' : child.name === 'media' ? `Move it out: \`@media (…) { ${selector} { … } }\` at the top level` : 'Move it out of the rule';
132
- reportInFile(child, 'invalid_layout_rule_content', `rule \`${selector}\`: ${what} can't be inside a layout rule, which holds only declarations and @origami-target blocks. ${fix} (tracker CONV-astParser.ts:74).`);
132
+ reportInFile(child, 'invalid_layout_rule_content', `rule \`${selector}\`: ${what} can't be inside a layout rule, which holds only declarations and @origami-target blocks. ${fix}.`);
133
133
  }
134
134
  }
135
135
  /** An `@origami-target`: a block (CONV-astParser.ts:51), a known name (:54), once per rule (:57), only declarations (:51), in the converter's order. */
136
136
  function checkOrigamiTarget(node, selector, seenTargets, reportInFile) {
137
137
  const target = node.params.trim();
138
- const reportTargetShape = (fix) => reportInFile(node, 'invalid_target_content', `rule \`${selector}\`: \`@origami-target ${target}\` holds only declarations, in a \`{ … }\` block. ${fix} (tracker CONV-astParser.ts:51).`);
138
+ const reportTargetShape = (fix) => reportInFile(node, 'invalid_target_content', `rule \`${selector}\`: \`@origami-target ${target}\` holds only declarations, in a \`{ … }\` block. ${fix}.`);
139
139
  if (node.nodes === undefined)
140
140
  return reportTargetShape("Give it a block with the container's declarations");
141
141
  if (!ORIGAMI_TARGETS.includes(target))
142
- return reportInFile(node, 'unknown_target', `rule \`${selector}\`: \`@origami-target ${target}\` is not a known target. Use \`@origami-target ${ORIGAMI_TARGETS[0]}\` or \`@origami-target ${ORIGAMI_TARGETS[1]}\` (tracker CONV-astParser.ts:54).`);
142
+ return reportInFile(node, 'unknown_target', `rule \`${selector}\`: \`@origami-target ${target}\` is not a known target. Use \`@origami-target ${ORIGAMI_TARGETS[0]}\` or \`@origami-target ${ORIGAMI_TARGETS[1]}\`.`);
143
143
  if (seenTargets.has(target))
144
- return reportInFile(node, 'duplicate_target', `rule \`${selector}\`: \`@origami-target ${target}\` appears twice. Merge the two blocks into one (tracker CONV-astParser.ts:57).`);
144
+ return reportInFile(node, 'duplicate_target', `rule \`${selector}\`: \`@origami-target ${target}\` appears twice. Merge the two blocks into one.`);
145
145
  seenTargets.add(target);
146
146
  const nested = getCssChildren(node).find((child) => child.type !== 'decl');
147
147
  if (!nested)
@@ -153,7 +153,7 @@ function checkStyleRule(rule, reportInFile) {
153
153
  const selector = rule.selector.trim();
154
154
  for (const child of getCssChildren(rule)) {
155
155
  if (child.type === 'atrule' && child.name !== 'apply') {
156
- reportInFile(child, 'at_rule_in_style_rule', `rule \`${selector}\`: \`@${child.name}\` can't be nested in a style rule. Put the rule inside it instead: \`@media (…) { ${selector} { … } }\` (in a @mixin: \`@result { @media (…) { &… { … } } }\`) (tracker CONV-astParser.ts:95).`);
156
+ reportInFile(child, 'at_rule_in_style_rule', `rule \`${selector}\`: \`@${child.name}\` can't be nested in a style rule. Put the rule inside it instead: \`@media (…) { ${selector} { … } }\` (in a @mixin: \`@result { @media (…) { &… { … } } }\`).`);
157
157
  }
158
158
  }
159
159
  }
@@ -86,7 +86,6 @@ const SPX_RESOLVER_TYPES = ['scale', 'fixed'];
86
86
  /** A property allowed at the top of a rule with `@origami-target` blocks (isRootLayoutProperty): the element's own, item and spx properties, and the root-target container property. */
87
87
  const isRootProperty = (prop) => prop in ELEMENT_PROPS || prop in ITEM_PROPS || SPX_PROPS.includes(prop) || CONTAINER_PROPS[prop]?.target === 'root';
88
88
  const isLayoutProperty = (prop) => isRootProperty(prop) || prop in CONTAINER_PROPS;
89
- const ROW = (line) => `(tracker CONV-layoutParser.ts:${line})`;
90
89
  export default defineRule({
91
90
  name: 'css-layout-properties',
92
91
  checks: CHECKS,
@@ -129,14 +128,14 @@ function checkPropertiesOnce(declarations, where, reportAt) {
129
128
  const seen = new Set();
130
129
  for (const decl of declarations) {
131
130
  if (seen.has(decl.prop))
132
- reportAt(decl, 'duplicate_property', `\`${decl.prop}\` is declared twice in ${where}: keep one ${ROW(498)}.`);
131
+ reportAt(decl, 'duplicate_property', `\`${decl.prop}\` is declared twice in ${where}: keep one.`);
133
132
  seen.add(decl.prop);
134
133
  }
135
134
  }
136
135
  function checkLayoutPropertiesOnly({ element, declarations }, reportAt) {
137
136
  for (const decl of declarations) {
138
137
  if (!isLayoutProperty(decl.prop))
139
- reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)} ${ROW(505)}.`);
138
+ reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)}.`);
140
139
  }
141
140
  }
142
141
  /** A property the layout tables don't have: in capitals, the import doesn't know it (it reads names as written), else it is paint for style.css. */
@@ -152,9 +151,9 @@ function checkRootPropertiesAtTop({ element, declarations }, reportAt) {
152
151
  continue;
153
152
  const container = CONTAINER_PROPS[decl.prop];
154
153
  if (container)
155
- reportAt(decl, 'property_outside_target', `${element}'s \`${decl.prop}\` goes inside \`@origami-target ${container.target} { … }\`, not at the top of its rule ${ROW(512)}.`);
154
+ reportAt(decl, 'property_outside_target', `${element}'s \`${decl.prop}\` goes inside \`@origami-target ${container.target} { … }\`, not at the top of its rule.`);
156
155
  else
157
- reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)} ${ROW(512)}.`);
156
+ reportAt(decl, 'non_layout_property', `${describeNonLayoutProperty(decl.prop, element)}.`);
158
157
  }
159
158
  }
160
159
  /** Each declaration of a target block is a container property of that target; the content block also takes `position: relative` (targetContract.ts). */
@@ -167,11 +166,11 @@ function checkTargetProperties(element, target, declarations, reportAt) {
167
166
  continue;
168
167
  if (container) {
169
168
  const place = container.target === 'root' ? `at the top of ${element}'s rule` : `in \`@origami-target ${container.target}\``;
170
- reportAt(decl, 'property_in_wrong_target', `${element}'s \`${decl.prop}\` goes ${place}, not in ${target} ${ROW(524)}.`);
169
+ reportAt(decl, 'property_in_wrong_target', `${element}'s \`${decl.prop}\` goes ${place}, not in ${target}.`);
171
170
  }
172
171
  else {
173
172
  const fix = isLayoutProperty(decl.prop) ? `move it to the top of ${element}'s rule` : `move it to ${element}'s rule in style.css`;
174
- reportAt(decl, 'non_container_target_property', `\`${decl.prop}\` in ${element}'s \`@origami-target ${target}\` block is not a container property: ${fix} ${ROW(524)}.`);
173
+ reportAt(decl, 'non_container_target_property', `\`${decl.prop}\` in ${element}'s \`@origami-target ${target}\` block is not a container property: ${fix}.`);
175
174
  }
176
175
  }
177
176
  }
@@ -184,7 +183,7 @@ function checkScope(scope, inherited, reportAt) {
184
183
  for (const [prop, decl] of values) {
185
184
  const value = decl.value.trim();
186
185
  if (value === '') {
187
- reportAt(decl, 'missing_property_value', `${element}'s \`${prop}\` has no value: write a value or remove the declaration ${ROW(430)}.`);
186
+ reportAt(decl, 'missing_property_value', `${element}'s \`${prop}\` has no value: write a value or remove the declaration.`);
188
187
  continue;
189
188
  }
190
189
  if (SPX_PROPS.includes(prop))
@@ -192,21 +191,21 @@ function checkScope(scope, inherited, reportAt) {
192
191
  const container = CONTAINER_PROPS[prop];
193
192
  if (container && type && !container[type]) {
194
193
  const [own, other] = type === 'grid' ? ['grid', 'flex'] : ['flex', 'grid'];
195
- reportAt(decl, 'incompatible_container_property', `${element} is a ${own} container (\`display: ${own}\`), and \`${prop}\` is a ${other} property: remove it, or use \`display: ${other}\` ${ROW(447)}.`);
194
+ reportAt(decl, 'incompatible_container_property', `${element} is a ${own} container (\`display: ${own}\`), and \`${prop}\` is a ${other} property: remove it, or use \`display: ${other}\`.`);
196
195
  continue;
197
196
  }
198
197
  if (prop === 'flex-wrap' && ownType === 'stack')
199
198
  continue; // :218 reported it; an inherited flex reads it as a value (:437)
200
199
  const spec = ELEMENT_PROPS[prop] ?? ITEM_PROPS[prop] ?? container?.spec;
201
200
  if (spec && !isReadable(spec, value))
202
- reportAt(decl, 'invalid_layout_value', `${element}'s \`${prop}: ${value}\` is not a layout value. Allowed: ${spec.allowed} ${ROW(437)}.`);
201
+ reportAt(decl, 'invalid_layout_value', `${element}'s \`${prop}: ${value}\` is not a layout value. Allowed: ${spec.allowed}.`);
203
202
  }
204
203
  if (scope.rulesWithTargets.length && !type) {
205
- reportAt(scope.rulesWithTargets[0], 'missing_container_display', `${element} has \`@origami-target\` blocks but no display: add \`display: grid\` or \`display: flex\` in its \`@origami-target ${CONTENT_TARGET}\` block ${ROW(451)}.`);
204
+ reportAt(scope.rulesWithTargets[0], 'missing_container_display', `${element} has \`@origami-target\` blocks but no display: add \`display: grid\` or \`display: flex\` in its \`@origami-target ${CONTENT_TARGET}\` block.`);
206
205
  }
207
206
  const gridRow = values.get('grid-row');
208
207
  if (gridRow && !values.has('grid-column')) {
209
- reportAt(gridRow, 'missing_grid_column', `${element} has \`grid-row\` but no \`grid-column\`: add \`grid-column: <start> / <end>\` (in the same @media, if the grid-row is in one) ${ROW(264)}.`);
208
+ reportAt(gridRow, 'missing_grid_column', `${element} has \`grid-row\` but no \`grid-column\`: add \`grid-column: <start> / <end>\` (in the same @media, if the grid-row is in one).`);
210
209
  }
211
210
  return type;
212
211
  }
@@ -222,7 +221,7 @@ function checkSpxReference({ element, values }, reportAt) {
222
221
  return; // :430 reports the empty one
223
222
  if ((refWidthValue === undefined || Number.isFinite(Number(refWidthValue))) && (resolverTypeValue === undefined || SPX_RESOLVER_TYPES.includes(resolverTypeValue)))
224
223
  return;
225
- reportAt(first, 'invalid_spx_context', `${element}'s spx context is invalid: \`--spx-ref-width\` takes a plain number (1280, no px) and \`--spx-resolver-type\` takes scale or fixed ${ROW(417)}.`);
224
+ reportAt(first, 'invalid_spx_context', `${element}'s spx context is invalid: \`--spx-ref-width\` takes a plain number (1280, no px) and \`--spx-resolver-type\` takes scale or fixed.`);
226
225
  }
227
226
  /** The container type the scope's own display gives (resolveContainerType): grid, or flex with no flex-wrap other than nowrap (:218). */
228
227
  function checkContainerType({ element, values }, reportAt) {
@@ -234,7 +233,7 @@ function checkContainerType({ element, values }, reportAt) {
234
233
  const wrap = values.get('flex-wrap');
235
234
  const wrapValue = wrap?.value.trim();
236
235
  if (wrap && wrapValue !== '' && wrapValue !== 'nowrap') {
237
- reportAt(wrap, 'unsupported_flex_wrap', `${element} has \`flex-wrap: ${wrapValue}\`, which isn't supported: remove it (or write \`nowrap\`), or use \`display: grid\` for items that wrap ${ROW(218)}.`);
236
+ reportAt(wrap, 'unsupported_flex_wrap', `${element} has \`flex-wrap: ${wrapValue}\`, which isn't supported: remove it (or write \`nowrap\`), or use \`display: grid\` for items that wrap.`);
238
237
  }
239
238
  return 'stack';
240
239
  }
@@ -270,7 +269,7 @@ function checkContainerParent(child, parent, { report }) {
270
269
  const parentName = nameOf(parent);
271
270
  const props = [...new Set(declarations.map(({ prop }) => prop))].join(', ');
272
271
  const line = Math.min(...declarations.map(({ decl }) => decl.line));
273
- 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 ${ROW(359)}.` });
272
+ 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.` });
274
273
  }
275
274
  }
276
275
  /** The parent's container type at this breakpoint, as the import derives it: its last `display` there (on itself or in its content-wrapper block) when it is grid or flex, else the default's. */
@@ -52,7 +52,7 @@ function checkScopeOwner(node, file) {
52
52
  const owner = SCOPE_OWNER.exec(params)?.[1];
53
53
  if (owner !== undefined && isSafeCssIdentifier(owner))
54
54
  return;
55
- file.reportAt(node, 'invalid_scope_owner', `\`@scope ${params}\` must name its owner as \`(.<class>)\`, one element's class. Write \`@scope (.${ownClassOf(owner ?? params.replace(/^[(.#\s]+/, ''))})\` (tracker CONV-utils.ts:70).`);
55
+ file.reportAt(node, 'invalid_scope_owner', `\`@scope ${params}\` must name its owner as \`(.<class>)\`, one element's class. Write \`@scope (.${ownClassOf(owner ?? params.replace(/^[(.#\s]+/, ''))})\`.`);
56
56
  }
57
57
  /** One rule's selector, in the import's order: the flat global style, the split, the parts, the first part, then the chain. */
58
58
  function checkSelector(rule, file) {
@@ -66,13 +66,13 @@ function checkSelector(rule, file) {
66
66
  return;
67
67
  const [first, ...rest] = parts;
68
68
  if (first === undefined)
69
- return file.reportAt(rule, 'empty_selector', 'the rule has an empty selector. Start it with the element\'s class: `.comp-a { … }` (tracker CONV-cssSelectors.ts:370).');
69
+ return file.reportAt(rule, 'empty_selector', 'the rule has an empty selector. Start it with the element\'s class: `.comp-a { … }`.');
70
70
  if (first.isAmpersand)
71
71
  return checkGlobalStyleChain(first, rest, selector, rule, file);
72
72
  if (file.inMixin)
73
73
  return;
74
74
  if (first.className === undefined)
75
- return file.reportAt(rule, 'missing_element_selector', `selector \`${selector}\` doesn't start with an element. Start it with the element's class: \`.comp-a${first.text}\` (tracker CONV-cssSelectors.ts:370).`);
75
+ return file.reportAt(rule, 'missing_element_selector', `selector \`${selector}\` doesn't start with an element. Start it with the element's class: \`.comp-a${first.text}\`.`);
76
76
  const element = splitStateClasses(first);
77
77
  if (!checkElementClass(element.className, first.className, selector, rule, file))
78
78
  return;
@@ -84,7 +84,7 @@ function checkSelector(rule, file) {
84
84
  function checkFlatGlobalStyle(selector, rule, file) {
85
85
  if (file.name === 'layout.css' || file.inMixin || !selector.startsWith('--'))
86
86
  return true;
87
- file.reportAt(rule, 'global_style_as_rule', `\`${selector} { … }\` is not a valid rule. Declare the global style as \`@mixin ${selector} { @result { … } }\` (tracker CONV-astParser.ts:88).`);
87
+ file.reportAt(rule, 'global_style_as_rule', `\`${selector} { … }\` is not a valid rule. Declare the global style as \`@mixin ${selector} { @result { … } }\`.`);
88
88
  return false;
89
89
  }
90
90
  /** Reports an element class that isn't a plain identifier or is a reserved name (CONV-cssSelectors.ts:54), naming the class as written. */
@@ -92,7 +92,7 @@ function checkElementClass(className, written, selector, rule, file) {
92
92
  if (isSafeCssIdentifier(className))
93
93
  return true;
94
94
  const problem = isCssIdentifier(className) ? `\`${className}\` is a reserved name, not an element class. Give the element another className` : `\`${written}\` is not an element class. ${howToWrite(written, ownClassOf(className))}`;
95
- file.reportAt(rule, 'invalid_selector_segment', `in \`${selector}\`, ${problem} (tracker CONV-cssSelectors.ts:54).`);
95
+ file.reportAt(rule, 'invalid_selector_segment', `in \`${selector}\`, ${problem}.`);
96
96
  return false;
97
97
  }
98
98
  /** A layout.css rule: one element's class (CONV-layoutParser.ts:105), no state (:108); a part with no class is refused first (CONV-cssSelectors.ts:213). */
@@ -102,10 +102,10 @@ function checkLayoutChain(first, rest, selector, rule, file) {
102
102
  const state = [first, ...rest].flatMap((part) => part.states)[0];
103
103
  if (rest.length) {
104
104
  const element = rest.findLast((part) => file.elementClassNames.has(baseClassOf(part))) ?? first;
105
- return file.reportAt(rule, 'multiple_layout_elements', `\`${selector}\` names more than one element; a layout rule takes one element's class: write \`.${baseClassOf(element)} { … }\` (layout can't be set on an inner part or through an ancestor) (tracker CONV-layoutParser.ts:105).`);
105
+ return file.reportAt(rule, 'multiple_layout_elements', `\`${selector}\` names more than one element; a layout rule takes one element's class: write \`.${baseClassOf(element)} { … }\` (layout can't be set on an inner part or through an ancestor).`);
106
106
  }
107
107
  if (state !== undefined)
108
- return file.reportAt(rule, 'layout_by_state', `\`${selector}\` sets layout by state, which isn't supported: remove \`${state}\` from the selector (state changes go in style.css) (tracker CONV-layoutParser.ts:108).`);
108
+ return file.reportAt(rule, 'layout_by_state', `\`${selector}\` sets layout by state, which isn't supported: remove \`${state}\` from the selector (state changes go in style.css).`);
109
109
  checkElementExists(first, rule, file);
110
110
  }
111
111
  /** A style or theme rule on an element (cssSelectors.ts parseComponentChain): a second element needs a state on the first (:321), carries
@@ -118,9 +118,9 @@ function checkElementChain(first, rest, selector, rule, file) {
118
118
  const second = splitStateClasses(rest[0]);
119
119
  const third = rest.slice(1).find((part) => isAnotherElement(part, file));
120
120
  if (third)
121
- return file.reportAt(rule, 'too_many_ancestor_elements', `\`${selector}\` chains three elements. Keep one ancestor: \`.${first.className}${first.states.join('')} .${baseClassOf(third)}\` (tracker CONV-cssSelectors.ts:318).`);
121
+ return file.reportAt(rule, 'too_many_ancestor_elements', `\`${selector}\` chains three elements. Keep one ancestor: \`.${first.className}${first.states.join('')} .${baseClassOf(third)}\`.`);
122
122
  if (!first.states.length)
123
- return file.reportAt(rule, 'ancestor_without_state', `\`${selector}\` puts one element under another without a state. Address the element by its own class (\`.${ownClassOf(second.className)}\`), or condition it on the ancestor's state (\`.${first.className}:hover .${ownClassOf(second.className)}\`) (tracker CONV-cssSelectors.ts:321).`);
123
+ return file.reportAt(rule, 'ancestor_without_state', `\`${selector}\` puts one element under another without a state. Address the element by its own class (\`.${ownClassOf(second.className)}\`), or condition it on the ancestor's state (\`.${first.className}:hover .${ownClassOf(second.className)}\`).`);
124
124
  if (second.states.length)
125
125
  return reportTwoOwners(first, second, selector, rule, file);
126
126
  if (!checkElementClass(second.className, rest[0].className, selector, rule, file))
@@ -149,14 +149,14 @@ function checkInnerPartsHaveNoId(inner, selector, rule, file) {
149
149
  const part = inner.find(hasId);
150
150
  if (part === undefined)
151
151
  return true;
152
- file.reportAt(rule, 'inner_part_has_id', `in \`${selector}\`, \`.${part.className}\` is not an inner part: an inner part's selector has no \`#\` id. Use an inner part's selector from the element's contract, without the id (\`.${ownClassOf(part.className)}\`) (tracker CONV-cssSelectors.ts:222).`);
152
+ file.reportAt(rule, 'inner_part_has_id', `in \`${selector}\`, \`.${part.className}\` is not an inner part: an inner part's selector has no \`#\` id. Use an inner part's selector from the element's contract, without the id (\`.${ownClassOf(part.className)}\`).`);
153
153
  return false;
154
154
  }
155
155
  /** A `@mixin`'s rule, `&…` (cssSelectors.ts parseGlobalStyleChain): outside a `@mixin` it's refused (:377); a later `&` or `:root` is the
156
156
  * global-styles rows'; then the inner parts, read as an element's (parseIeChain), with no id (:222). */
157
157
  function checkGlobalStyleChain(first, rest, selector, rule, file) {
158
158
  if (!file.inMixin)
159
- return file.reportAt(rule, 'parent_selector_outside_mixin', `\`${selector}\` is outside a \`@mixin\`. Replace \`&\` with the element's class (\`.comp-a${selector.slice(1)}\`), or move the rule into the global style's \`@mixin\` (tracker CONV-cssSelectors.ts:377).`);
159
+ return file.reportAt(rule, 'parent_selector_outside_mixin', `\`${selector}\` is outside a \`@mixin\`. Replace \`&\` with the element's class (\`.comp-a${selector.slice(1)}\`), or move the rule into the global style's \`@mixin\`.`);
160
160
  if (rest.some((part) => part.isAmpersand || part.text === ':root'))
161
161
  return;
162
162
  if (!checkInnerParts(rest, first, selector, rule, file))
@@ -171,7 +171,7 @@ function checkInnerParts(inner, owner, selector, rule, file) {
171
171
  return false;
172
172
  const stated = inner.filter((part) => part.states.length);
173
173
  if (stated.length > 1) {
174
- file.reportAt(rule, 'multiple_state_owners', `in \`${selector}\`, states sit on \`${stated[0].text}\` and \`${stated[1].text}\`. Attach the states to the one part that owns them (tracker CONV-cssSelectors.ts:213).`);
174
+ file.reportAt(rule, 'multiple_state_owners', `in \`${selector}\`, states sit on \`${stated[0].text}\` and \`${stated[1].text}\`. Attach the states to the one part that owns them.`);
175
175
  return false;
176
176
  }
177
177
  if (stated.length && owner.states.length) {
@@ -185,19 +185,19 @@ function checkPartsHaveClass(parts, selector, rule, file) {
185
185
  const bare = parts.find((part) => part.className === undefined);
186
186
  if (!bare)
187
187
  return true;
188
- file.reportAt(rule, 'state_without_class', `in \`${selector}\`, \`${bare.text}\` has no class. Attach the state to the part that owns it (\`.comp-a:hover\`, \`.comp-a .label:hover\`) and write every later part as a class (tracker CONV-cssSelectors.ts:213).`);
188
+ file.reportAt(rule, 'state_without_class', `in \`${selector}\`, \`${bare.text}\` has no class. Attach the state to the part that owns it (\`.comp-a:hover\`, \`.comp-a .label:hover\`) and write every later part as a class.`);
189
189
  return false;
190
190
  }
191
191
  /** Reports states on two owners (CONV-cssSelectors.ts:216). */
192
192
  function reportTwoOwners(owner, other, selector, rule, file) {
193
193
  const name = (part) => (part.isAmpersand ? '&' : `.${baseClassOf(part)}`);
194
- file.reportAt(rule, 'multiple_state_owners', `\`${selector}\` puts states on two owners (\`${name(owner)}\` and \`${name(other)}\`). Condition the rule on one owner's states only (tracker CONV-cssSelectors.ts:216).`);
194
+ file.reportAt(rule, 'multiple_state_owners', `\`${selector}\` puts states on two owners (\`${name(owner)}\` and \`${name(other)}\`). Condition the rule on one owner's states only.`);
195
195
  }
196
196
  /** Reports a rule for an element that structure.jsx doesn't have (CONV-fromBundle.ts:855). */
197
197
  function checkElementExists(element, rule, file) {
198
198
  if (file.inMixin || file.elementClassNames.has(element.className))
199
199
  return;
200
- file.reportAt(rule, 'missing_selector_element', `\`.${element.className}\` styles no element: no element in structure.jsx has className="${element.className}". Remove the rule, or write the className of an element (tracker CONV-fromBundle.ts:855).`);
200
+ file.reportAt(rule, 'missing_selector_element', `\`.${element.className}\` styles no element: no element in structure.jsx has className="${element.className}". Remove the rule, or write the className of an element.`);
201
201
  }
202
202
  // ── reading a selector ──
203
203
  /** The selector's parts, split as the import splits them (getSelectorSegments, cssSelectors.ts splitSegments), each read as the import
@@ -229,14 +229,14 @@ function reportUnsplittable(selector, rule, file) {
229
229
  else if ((char === ']' || char === ')') && !openers.pop())
230
230
  return reportUnbalanced(`has a \`${char}\` with no \`${char === ']' ? '[' : '('}\` before it`, selector, rule, file);
231
231
  else if (char === ',' && !openers.length) {
232
- file.reportAt(rule, 'selector_list', `\`${selector}\` is a selector list. Split it into one rule per selector (tracker CONV-cssSelectors.ts:92).`);
232
+ file.reportAt(rule, 'selector_list', `\`${selector}\` is a selector list. Split it into one rule per selector.`);
233
233
  return null;
234
234
  }
235
235
  }
236
236
  return reportUnbalanced(inQuotes ? 'has an unclosed `"`' : `has an unclosed \`${openers.at(-1)}\``, selector, rule, file);
237
237
  }
238
238
  function reportUnbalanced(what, selector, rule, file) {
239
- file.reportAt(rule, 'unclosed_selector_token', `selector \`${selector}\` ${what}. Close the bracket or quote (tracker CONV-cssSelectors.ts:87).`);
239
+ file.reportAt(rule, 'unclosed_selector_token', `selector \`${selector}\` ${what}. Close the bracket or quote.`);
240
240
  return null;
241
241
  }
242
242
  /** One part as the import reads it: `:root`, or `&` or a class, then states (`:hover`, `.state-class`); an attribute or functional
@@ -246,7 +246,7 @@ function readPart(text, selector, rule, file) {
246
246
  return { text, isAmpersand: false, states: [] };
247
247
  const { base, states, unread } = readSegment(text);
248
248
  if (unread) {
249
- file.reportAt(rule, 'unsupported_selector_syntax', `in \`${selector}\`, the format can't read \`${unread}\`. ${howToWrite(unread, 'comp-a')} (tracker CONV-cssSelectors.ts:169).`);
249
+ file.reportAt(rule, 'unsupported_selector_syntax', `in \`${selector}\`, the format can't read \`${unread}\`. ${howToWrite(unread, 'comp-a')}.`);
250
250
  return null;
251
251
  }
252
252
  return { text, isAmpersand: base === '&', className: base === '&' ? undefined : base?.slice(1), states };
@@ -38,7 +38,7 @@ function checkMixin(mixin, reportInFile) {
38
38
  }
39
39
  const result = getResult(mixin);
40
40
  if (!result) {
41
- reportInFile(mixin, 'invalid_mixin_result', `\`@mixin --${id}\` must be \`@mixin --${id} { @result { … } }\`. Put everything inside one \`@result { }\` (tracker CONV-astParser.ts:293).`);
41
+ reportInFile(mixin, 'invalid_mixin_result', `\`@mixin --${id}\` must be \`@mixin --${id} { @result { … } }\`. Put everything inside one \`@result { }\`.`);
42
42
  return [];
43
43
  }
44
44
  const rules = [];
@@ -59,12 +59,12 @@ function checkMixinRule(node, id, breakpoint, rules, reportInFile) {
59
59
  const selector = node.selector.trim();
60
60
  const inner = getCssChildren(node);
61
61
  if (!selector.startsWith('&'))
62
- return reportInFile(node, 'missing_parent_selector', `\`@mixin --${id}\`: selector \`${selector}\` must start with \`&\`. Write \`& ${selector}\` (or \`&:hover\`) (tracker CONV-astParser.ts:195).`);
62
+ return reportInFile(node, 'missing_parent_selector', `\`@mixin --${id}\`: selector \`${selector}\` must start with \`&\`. Write \`& ${selector}\` (or \`&:hover\`).`);
63
63
  if (selector === '&')
64
64
  return reportInFile(node, 'bare_parent_selector', getBareAmpersandMessage(id, inner));
65
65
  const nested = inner.find((child) => child.type === 'rule');
66
66
  if (nested)
67
- return reportInFile(nested, 'nested_style_rule', `\`@mixin --${id}\`: a rule is nested in \`${selector}\`. Write the whole selector on one rule: \`${joinSelectors(selector, nested.selector.trim())} { … }\` (tracker CONV-astParser.ts:204).`);
67
+ return reportInFile(nested, 'nested_style_rule', `\`@mixin --${id}\`: a rule is nested in \`${selector}\`. Write the whole selector on one rule: \`${joinSelectors(selector, nested.selector.trim())} { … }\`.`);
68
68
  const atRules = inner.filter((child) => child.type === 'atrule');
69
69
  const applies = atRules.filter((child) => child.name === 'apply');
70
70
  // another at-rule (CONV-astParser.ts:211) is css-file-nesting's; two `@apply`s, one beside declarations, or one that isn't
@@ -76,12 +76,12 @@ function checkMixinRule(node, id, breakpoint, rules, reportInFile) {
76
76
  /** An at-rule in `@result`: no `@apply` (:246), no `@mixin` (:251); only `@media` / `@scope` at its top, none inside them (:254). */
77
77
  function checkResultAtRule(node, id, breakpoint, rules, reportInFile) {
78
78
  if (node.name === 'apply')
79
- return reportInFile(node, 'apply_at_result_root', `\`@mixin --${id}\`: \`@apply\` can't sit at the root of \`@result\`. Remove it, or move it into an inner part's rule: \`& .label { @apply ${node.params.trim()}; }\` (tracker CONV-astParser.ts:246).`);
79
+ return reportInFile(node, 'apply_at_result_root', `\`@mixin --${id}\`: \`@apply\` can't sit at the root of \`@result\`. Remove it, or move it into an inner part's rule: \`& .label { @apply ${node.params.trim()}; }\`.`);
80
80
  if (node.name === 'mixin')
81
- return reportInFile(node, 'nested_mixin', `\`@mixin --${id}\`: \`@mixin ${node.params.trim()}\` is declared inside it. Declare each \`@mixin\` at the top level of the file (tracker CONV-astParser.ts:251).`);
81
+ return reportInFile(node, 'nested_mixin', `\`@mixin --${id}\`: \`@mixin ${node.params.trim()}\` is declared inside it. Declare each \`@mixin\` at the top level of the file.`);
82
82
  if (breakpoint || !BREAKPOINT_AT_RULES.includes(node.name)) {
83
83
  const where = breakpoint ? `\`${describeAtRule(breakpoint)}\` in \`@result\`` : '`@result`';
84
- return reportInFile(node, 'invalid_result_at_rule', `\`@mixin --${id}\`: \`@${node.name}\` is not allowed in ${where}. Use only \`@media\` / \`@scope\` directly inside \`@result\`, one level deep (tracker CONV-astParser.ts:254).`);
84
+ return reportInFile(node, 'invalid_result_at_rule', `\`@mixin --${id}\`: \`@${node.name}\` is not allowed in ${where}. Use only \`@media\` / \`@scope\` directly inside \`@result\`, one level deep.`);
85
85
  }
86
86
  const media = getBreakpointBody(node);
87
87
  if (media)
@@ -103,7 +103,7 @@ function checkSelector(rule, reportInFile) {
103
103
  if (repeated === undefined)
104
104
  return true;
105
105
  const problem = repeated === ':root' ? 'has `:root` after `&`' : 'repeats `&`';
106
- reportInFile(rule.node, 'invalid_parent_selector', `\`@mixin --${rule.mixin}\`: selector \`${rule.selector}\` ${problem}. Write \`&\` once, at the start of the selector (tracker CONV-cssSelectors.ts:351).`);
106
+ reportInFile(rule.node, 'invalid_parent_selector', `\`@mixin --${rule.mixin}\`: selector \`${rule.selector}\` ${problem}. Write \`&\` once, at the start of the selector.`);
107
107
  return false;
108
108
  }
109
109
  /** Each link, in the import's order (collectGlobalStyleLinks): no state (:655), no breakpoint (:664), a dashed id (:670) the file declares (:673), once per part (:677). Returns the parts a link names, by mixin. */
@@ -116,26 +116,26 @@ function checkLinks(links, declaredIds, reportInFile) {
116
116
  const linkDescription = `\`@mixin --${link.mixin}\`'s link \`@apply ${child}\``;
117
117
  const fix = `write it as \`${getSelectorWithoutStates(link.selector)} { @apply ${child} }\``;
118
118
  if (hasState(link.selector)) {
119
- reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} and put the state / breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
119
+ reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} and put the state / breakpoint values in @mixin ${child}.`);
120
120
  continue;
121
121
  }
122
122
  if (link.breakpoint) {
123
- reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} outside \`${describeAtRule(link.breakpoint)}\`, and put the breakpoint values in @mixin ${child} (tracker CONV-styleParser.ts:655).`);
123
+ reportInFile(link.apply, 'conditional_global_style_link', `${linkDescription} can't be under a state or a breakpoint: ${fix} outside \`${describeAtRule(link.breakpoint)}\`, and put the breakpoint values in @mixin ${child}.`);
124
124
  continue;
125
125
  }
126
126
  const part = getPartKey(link);
127
127
  linkedParts.add(part);
128
128
  const id = getDashedId(child);
129
129
  if (!isSafeCssIdentifier(id)) {
130
- reportInFile(link.apply, 'invalid_linked_style_id', `\`@mixin --${link.mixin}\` links \`${child}\`, which is not a valid global style name: use letters, digits, - and _ (tracker CONV-styleParser.ts:670).`);
130
+ reportInFile(link.apply, 'invalid_linked_style_id', `\`@mixin --${link.mixin}\` links \`${child}\`, which is not a valid global style name: use letters, digits, - and _.`);
131
131
  continue;
132
132
  }
133
133
  if (!declaredIds.has(id)) {
134
- reportInFile(link.apply, 'missing_linked_style', `\`@mixin --${link.mixin}\` links \`${child}\`, but the file has no \`@mixin ${child}\`: add it (tracker CONV-styleParser.ts:673).`);
134
+ reportInFile(link.apply, 'missing_linked_style', `\`@mixin --${link.mixin}\` links \`${child}\`, but the file has no \`@mixin ${child}\`: add it.`);
135
135
  continue;
136
136
  }
137
137
  if (validLinkParts.has(part)) {
138
- reportInFile(link.apply, 'duplicate_global_style_link', `\`@mixin --${link.mixin}\` links two global styles at \`${getPart(link.selector)}\`: keep one (tracker CONV-styleParser.ts:677).`);
138
+ reportInFile(link.apply, 'duplicate_global_style_link', `\`@mixin --${link.mixin}\` links two global styles at \`${getPart(link.selector)}\`: keep one.`);
139
139
  continue;
140
140
  }
141
141
  validLinkParts.add(part);
@@ -175,7 +175,7 @@ function checkLinkCycles(validLinks, reportInFile) {
175
175
  }
176
176
  }
177
177
  function reportCycle(link, child, chain, reportInFile) {
178
- reportInFile(link.apply, 'mixin_link_cycle', `\`@mixin --${link.mixin}\` links \`--${child}\`, which leads back to it: ${chain.map((id) => `--${id}`).join(' → ')}. A global style can't contain itself: remove this link, or link a style that doesn't lead back to \`--${child}\` (tracker OUR-007).`);
178
+ reportInFile(link.apply, 'mixin_link_cycle', `\`@mixin --${link.mixin}\` links \`--${child}\`, which leads back to it: ${chain.map((id) => `--${id}`).join(' → ')}. A global style can't contain itself: remove this link, or link a style that doesn't lead back to \`--${child}\`.`);
179
179
  }
180
180
  /** One selector of a mixin (across its breakpoints): a link alone, or values; values on an inner part where a link names it (CONV-styleParser.ts:803, :698). */
181
181
  function checkLinkOrValues(group, linkedParts, reportInFile) {
@@ -183,12 +183,12 @@ function checkLinkOrValues(group, linkedParts, reportInFile) {
183
183
  const values = group.filter((rule) => !rule.apply);
184
184
  const [first] = group;
185
185
  if (link && values.length > 0) {
186
- return reportInFile(values[0].node, 'link_with_values', `\`${first.selector}\` of \`@mixin --${first.mixin}\` has both \`@apply ${link.apply.params.trim()}\` and values: move the values into @mixin ${link.apply.params.trim()} (tracker CONV-styleParser.ts:803).`);
186
+ return reportInFile(values[0].node, 'link_with_values', `\`${first.selector}\` of \`@mixin --${first.mixin}\` has both \`@apply ${link.apply.params.trim()}\` and values: move the values into @mixin ${link.apply.params.trim()}.`);
187
187
  }
188
188
  const part = getPart(first.selector);
189
189
  if (link || part === '' || linkedParts.has(getPartKey(first)))
190
190
  return;
191
- reportInFile(first.node, 'missing_global_style_link', `\`@mixin --${first.mixin}\` styles \`${part}\` but links no global style there: add \`& ${part} { @apply --<child> }\` and move the values to that child's @mixin (tracker CONV-styleParser.ts:698).`);
191
+ reportInFile(first.node, 'missing_global_style_link', `\`@mixin --${first.mixin}\` styles \`${part}\` but links no global style there: add \`& ${part} { @apply --<child> }\` and move the values to that child's @mixin.`);
192
192
  }
193
193
  // ── reading a mixin ──
194
194
  /** The `@mixin` a rule of its `@result` sits in. */
@@ -244,15 +244,15 @@ const joinSelectors = (outer, inner) => (inner.includes('&') ? inner.replaceAll(
244
244
  function getMixinIdMessage(params) {
245
245
  const written = getDashedId(params);
246
246
  if (written !== undefined && isCssIdentifier(written) && !isSafeCssIdentifier(written))
247
- return `\`@mixin ${params}\`: \`${written}\` is reserved. Name the global style with another dashed identifier (\`@mixin --${written.replace(/^_+|_+$/g, '')}-style\`) (tracker CONV-astParser.ts:287).`;
247
+ return `\`@mixin ${params}\`: \`${written}\` is reserved. Name the global style with another dashed identifier (\`@mixin --${written.replace(/^_+|_+$/g, '')}-style\`).`;
248
248
  const id = /^-*([_a-zA-Z][_a-zA-Z0-9-]*)/.exec(params)?.[1] ?? 'id';
249
249
  const state = /[:[]/.test(params) ? `. Write the state as an \`&${params.slice(params.search(/[:[]/))} { … }\` rule inside \`@result\`` : '';
250
- return `\`@mixin ${params}\` must name one dashed identifier (\`@mixin --${id}\`)${state} (tracker CONV-astParser.ts:287).`;
250
+ return `\`@mixin ${params}\` must name one dashed identifier (\`@mixin --${id}\`)${state}.`;
251
251
  }
252
252
  /** A bare `& { … }` (CONV-astParser.ts:198): its values go straight in `@result`; an `@apply` in it is the import's :658 case, refused here first. */
253
253
  function getBareAmpersandMessage(id, inner) {
254
254
  const apply = inner.find((child) => child.type === 'atrule' && child.name === 'apply');
255
255
  if (apply)
256
- return `\`@mixin --${id}\` applies ${apply.params.trim()} to itself, which isn't supported: write the values in @mixin --${id}'s @result, or link a child on an inner part (\`& .label { @apply … }\`) (tracker CONV-astParser.ts:198, CONV-styleParser.ts:658).`;
257
- return `\`@mixin --${id}\`: drop the \`& { }\` wrapper and write its declarations directly in \`@result\` (tracker CONV-astParser.ts:198).`;
256
+ return `\`@mixin --${id}\` applies ${apply.params.trim()} to itself, which isn't supported: write the values in @mixin --${id}'s @result, or link a child on an inner part (\`& .label { @apply … }\`).`;
257
+ return `\`@mixin --${id}\`: drop the \`& { }\` wrapper and write its declarations directly in \`@result\`.`;
258
258
  }
@@ -1,9 +1,9 @@
1
1
  import { STRUCTURE } from "../../rules-api/index.js";
2
2
  import { defineRule } from "./define-rule.js";
3
3
  const CHECKS = ['invalid_structure_declaration', 'free_code_outside_structure', 'typescript_syntax'];
4
- const FREE_CODE = 'The import refuses this statement: a structure file holds only imports, `const` / `let` / `var` declarations and `export const Structure = () => …` (tracker CONV-parseJsx.ts:287).';
5
- const STRUCTURE_SHAPE = 'Declare Structure once, as `export const Structure = () => ( <Page …/> )`: an exported const arrow that returns the root element directly, with no `{ return … }`, fragment or condition around it (tracker CONV-parseJsx.ts:335).';
6
- const typeScript = (code) => `\`${code}\` is TypeScript. The import reads a structure file as plain JavaScript and fails on it: remove it (tracker CONV-parseJsx.ts:1845).`;
4
+ const FREE_CODE = 'The import refuses this statement: a structure file holds only imports, `const` / `let` / `var` declarations and `export const Structure = () => …`.';
5
+ const STRUCTURE_SHAPE = 'Declare Structure once, as `export const Structure = () => ( <Page …/> )`: an exported const arrow that returns the root element directly, with no `{ return … }`, fragment or condition around it.';
6
+ const typeScript = (code) => `\`${code}\` is TypeScript. The import reads a structure file as plain JavaScript and fails on it: remove it.`;
7
7
  /** Every statement type but ImportDeclaration (always allowed) and the TypeScript ones (TYPESCRIPT). */
8
8
  const STATEMENTS = [
9
9
  'ExpressionStatement', 'ExportNamedDeclaration', 'ExportDefaultDeclaration', 'ExportAllDeclaration', 'VariableDeclaration',
@@ -78,7 +78,7 @@ function hasRoot({ rulesApi, report }) {
78
78
  if (rulesApi.page.kind === 'page')
79
79
  report({ check: 'missing_root', loc: FILE_START, message: 'A page must start with <Page>, found nothing.' });
80
80
  else
81
- report({ check: 'missing_root', loc: FILE_START, message: 'A global component must start with a section (HeaderSection, FooterSection or Section), found nothing (tracker CONV-runValidation.ts:9).' });
81
+ report({ check: 'missing_root', loc: FILE_START, message: 'A global component must start with a section (HeaderSection, FooterSection or Section), found nothing.' });
82
82
  }
83
83
  /** On a global component, reports a top element that isn't a section (P1-R13); the steps after it are for a section. */
84
84
  function isSectionAtTop({ node, rulesApi, report }) {
@@ -87,7 +87,7 @@ function isSectionAtTop({ node, rulesApi, report }) {
87
87
  const type = rulesApi.getType(node);
88
88
  if (isLandmark(node, rulesApi) || (type && SECTIONS.includes(type)))
89
89
  return true;
90
- report({ check: 'invalid_global_root', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} sits directly in the master page. A global component's top element is a section (HeaderSection, FooterSection or Section); put it inside one (rule P1-R13).` });
90
+ report({ check: 'invalid_global_root', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} sits directly in the master page. A global component's top element is a section (HeaderSection, FooterSection or Section); put it inside one.` });
91
91
  return false;
92
92
  }
93
93
  /** Reports a root element with no string id (CONV-runValidation.ts:9). The tree steps go on: the import reports them all in one run. */
@@ -100,7 +100,7 @@ function rootHasId({ node, rulesApi, report }) {
100
100
  && attribute.value.expression.name === 'id') && receivesInstanceId(node))
101
101
  return;
102
102
  const tag = rulesApi.getTagName(node);
103
- report({ check: 'missing_root_id', loc: getLoc(node, rulesApi), message: `<${tag}> at the top has no id. Give the root element its id, like \`<${tag} id="c1dmp">\` (tracker CONV-runValidation.ts:9).` });
103
+ report({ check: 'missing_root_id', loc: getLoc(node, rulesApi), message: `<${tag}> at the top has no id. Give the root element its id, like \`<${tag} id="c1dmp">\`.` });
104
104
  }
105
105
  /** Reports a page's root that isn't <Page>. */
106
106
  function startsWithPage({ node, rulesApi, report }) {
@@ -114,7 +114,7 @@ function checkPageChildren({ node: page, rulesApi, report }) {
114
114
  for (const child of getStructuralChildren(page, rulesApi)) {
115
115
  if (child.type === 'JSXElement')
116
116
  continue;
117
- report({ check: 'non_element_child', loc: getLoc(page, rulesApi), message: `<Page> has ${describe(child, rulesApi)} as a child. Only elements go directly inside <Page>: a section, or a component pinned to the page (tracker CONV-parseJsx.ts:1668).` });
117
+ report({ check: 'non_element_child', loc: getLoc(page, rulesApi), message: `<Page> has ${describe(child, rulesApi)} as a child. Only elements go directly inside <Page>: a section, or a component pinned to the page.` });
118
118
  }
119
119
  }
120
120
  /** Reports a section, anywhere in the file, that doesn't hold exactly one <InternalContainer>, and nothing else (S2-SECTION-001, OUR-001). A wrapper in it
@@ -127,7 +127,7 @@ function hasOneInternalContainer(section, { rulesApi, report }) {
127
127
  if (inside.length === 1 && inside[0].type === 'JSXElement' && rulesApi.getType(inside[0]) === 'innerElement')
128
128
  return;
129
129
  const name = rulesApi.getId(section) ?? `<${rulesApi.getTagName(section)}>`;
130
- report({ check: 'invalid_section_container', loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}. Put the section's content inside its one <InternalContainer> (rule S2-SECTION-001).` });
130
+ report({ check: 'invalid_section_container', loc: getLoc(section, rulesApi), message: `Section ${name} must contain exactly one <InternalContainer>, found: ${inside.map((child) => describe(child, rulesApi)).join(', ') || 'nothing'}. Put the section's content inside its one <InternalContainer>.` });
131
131
  }
132
132
  /** Reports a <Page> held by another element (P1-R16), unless the root steps above report that placement already (a section's children). */
133
133
  function isNotNestedPage({ node, rulesApi, report }) {
@@ -136,7 +136,7 @@ function isNotNestedPage({ node, rulesApi, report }) {
136
136
  const holder = getHolder(node, rulesApi);
137
137
  if (!holder || (rulesApi.getType(holder) !== 'page' && isRootTreeHolder(holder, rulesApi) && isStructuralChild(holder, node, rulesApi)))
138
138
  return;
139
- report({ check: 'nested_page', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} is inside ${describeElement(holder, rulesApi)}. A page is only the top element of its own page (rule P1-R16).` });
139
+ report({ check: 'nested_page', loc: getLoc(node, rulesApi), message: `${describeElement(node, rulesApi)} is inside ${describeElement(holder, rulesApi)}. A page is only the top element of its own page.` });
140
140
  }
141
141
  /** Reports a header or footer, or a page's reference to the global one, with an <InternalContainer> among its ancestors (P1-R03). */
142
142
  function isOutsideInternalContainers({ node, rulesApi, report }) {
@@ -146,20 +146,20 @@ function isOutsideInternalContainers({ node, rulesApi, report }) {
146
146
  const container = getAncestors(node).find((ancestor) => rulesApi.getType(ancestor) === 'innerElement');
147
147
  if (!container)
148
148
  return;
149
- report({ check: 'landmark_inside_container', loc: getLoc(node, rulesApi), message: `The ${kind} ${describeElement(node, rulesApi)} is inside the internal container ${describeElement(container, rulesApi)}. Move it out of every internal container: the header and footer sit directly in the page or the master page (rule P1-R03).` });
149
+ report({ check: 'landmark_inside_container', loc: getLoc(node, rulesApi), message: `The ${kind} ${describeElement(node, rulesApi)} is inside the internal container ${describeElement(container, rulesApi)}. Move it out of every internal container: the header and footer sit directly in the page or the master page.` });
150
150
  }
151
151
  /** Reports a wrapper that doesn't hold exactly one element: none, two, text, or a {…} (a comment too) beside it (CONV-parseJsx.ts:1028). */
152
152
  function holdsOneElement({ node, rulesApi, report }) {
153
153
  if (holdsOne(node))
154
154
  return;
155
- report({ check: 'invalid_wrapper_children', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> wraps exactly one component element: give each component its own wrapper (tracker CONV-parseJsx.ts:1028).` });
155
+ report({ check: 'invalid_wrapper_children', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> wraps exactly one component element: give each component its own wrapper.` });
156
156
  }
157
157
  /** Reports a <Remove> right inside another <Remove> (CONV-parseJsx.ts:1237): <Remove> wraps the component itself, so nothing nests in it. Compared with the element right around it, as the import does. */
158
158
  function checkNoRemoveInRemove({ node, rulesApi, report }) {
159
159
  const outer = getAncestors(node)[0];
160
160
  if (!outer || rulesApi.getType(outer) !== 'remove')
161
161
  return;
162
- report({ check: 'nested_remove', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> is inside another <Remove>. <Remove> wraps the component itself: merge the two conditions into one <Remove> (tracker CONV-parseJsx.ts:1237).` });
162
+ report({ check: 'nested_remove', loc: node.openingElement.name.loc, message: `<${rulesApi.getTagName(node)}> is inside another <Remove>. <Remove> wraps the component itself: merge the two conditions into one <Remove>.` });
163
163
  }
164
164
  /** Reports text, a {…} (a comment too), a fragment or a spread between a component's tags (CONV-parseJsx.ts:1668). The root steps report those in <Page> and its sections, except comments, which they don't see. */
165
165
  function hasOnlyElements({ node, rulesApi, report }) {
@@ -169,7 +169,7 @@ function hasOnlyElements({ node, rulesApi, report }) {
169
169
  continue;
170
170
  if (commentsOnly && !isComment(child))
171
171
  continue;
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).` });
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.` });
173
173
  }
174
174
  }
175
175
  /** Reports an <InternalContainer>'s or a <Page>'s layer that isn't an array literal of elements (CONV-parseJsx.ts:1630). */
@@ -182,7 +182,7 @@ function hasLayerArrays({ node, rulesApi, report }) {
182
182
  continue;
183
183
  if (isArrayOfElements(attribute))
184
184
  continue;
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).` });
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}={[<… />]}.` });
186
186
  }
187
187
  }
188
188
  /** A <PinnedToScreen>: no props, one element held, a pinnable one, in its <Page> slot. */