@wix/origami-lint 1.34.27 → 1.34.29
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/project/home-page-folder.js +3 -3
- package/dist/rules/site-app/apply-and-presets.js +13 -13
- package/dist/rules/site-app/bindings.js +135 -68
- package/dist/rules/site-app/breakpoints.js +6 -6
- package/dist/rules/site-app/css-file-nesting.js +13 -13
- package/dist/rules/site-app/css-layout-properties.js +14 -15
- package/dist/rules/site-app/css-selectors.js +18 -18
- package/dist/rules/site-app/global-styles.js +19 -19
- package/dist/rules/site-app/no-free-code.js +3 -3
- package/dist/rules/site-app/page-structure.js +11 -11
- package/dist/rules/site-app/slot-attributes.js +4 -4
- package/dist/rules/site-app/slot-placement.js +3 -3
- package/dist/rules/site-app/theme-font-ids.js +7 -49
- package/dist/rules/site-app/theme-font-known.js +30 -0
- package/dist/rules/site-app/theme-root.js +7 -8
- package/dist/rules-api/index.js +2 -2
- package/dist/rules-api/rules-api.js +2 -1
- package/dist/shared/constants.js +4 -0
- package/dist/shared/css.js +54 -0
- package/package.json +2 -2
|
@@ -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
|
|
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)}
|
|
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 { … }\`)
|
|
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
|
|
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
|
|
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
|
|
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 { … } }
|
|
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
|
|
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}
|
|
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}
|
|
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]}
|
|
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
|
|
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 (…) { &… { … } } }\`)
|
|
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
|
|
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)}
|
|
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
|
|
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)}
|
|
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}
|
|
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}
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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]+/, ''))})
|
|
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 { … }
|
|
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}
|
|
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 { … } }
|
|
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}
|
|
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)
|
|
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)
|
|
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)}
|
|
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)}\`)
|
|
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)}\`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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')}
|
|
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 { }
|
|
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\`)
|
|
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())} { … }
|
|
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()}; }
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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 _
|
|
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
|
|
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
|
|
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}
|
|
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()}
|
|
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
|
|
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\`)
|
|
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}
|
|
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 … }\`)
|
|
257
|
-
return `\`@mixin --${id}\`: drop the \`& { }\` wrapper and write its declarations directly in \`@result
|
|
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 = () =>
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}={[<… />]}
|
|
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. */
|